Skip to content

Instantly share code, notes, and snippets.

@junkdog
Last active July 17, 2025 09:21
Show Gist options
  • Save junkdog/f0e7a69f0c29ec56b058537775357147 to your computer and use it in GitHub Desktop.
Save junkdog/f0e7a69f0c29ec56b058537775357147 to your computer and use it in GitHub Desktop.

Building Custom EffectRegistry Types with EffectManager

A comprehensive guide to organizing and managing effects in terminal applications using tachyonfx's EffectManager with custom registry patterns.

Table of Contents


1. Introduction & Overview

What is the EffectManager and why build a custom registry

The EffectManager is the foundational component in tachyonfx that manages the lifecycle of effects in terminal applications. It handles effect processing, automatic cleanup of completed effects, and provides unique effect identification to prevent conflicts.

Note on Examples: This guide starts with &'static str IDs in the basic examples for readability without runtime overhead. As you progress, we'll introduce enum-based IDs for better type safety and compile-time guarantees in production code.

While the EffectManager is powerful on its own, building a custom registry on top of it provides several advantages:

  • Centralized Effect Management: All effects are coordinated through a single interface
  • Application-Specific APIs: Create methods tailored to your application's needs
  • Event Integration: Connect effects directly to your application's event system
  • Type Safety: Use enums for effect IDs to prevent typos and ensure consistency
  • Advanced Features: Add screen area tracking, dynamic effects, and custom utilities

Benefits of the registry pattern for effect organization

The registry pattern provides a clean abstraction layer between your application logic and the effect system:

  1. Separation of Concerns: Effect management is isolated from UI rendering logic
  2. Consistency: Standardized approach to effect registration and management
  3. Extensibility: Easy to add new effect types and behaviors
  4. Maintainability: Centralized location for all effect-related code
  5. Testing: Easier to test effect behavior in isolation

Real-world example: glim app's EffectRegistry

The glim TUI application employs a EffectRegistry implementation that manages:

  • Popup window animations with background dimming
  • Glitch effects with varying intensities
  • Notification systems with complex fade/blink sequences
  • Dynamic area tracking for responsive effects
  • Event-driven effect triggering

This guide will show you how to build similar functionality for your own applications.


2. Core Concepts

Understanding the EffectManager foundation

The EffectManager provides the core functionality for effect lifecycle management:

use tachyonfx::{EffectManager, Effect, Duration, fx, Motion};
use ratatui::{buffer::Buffer, layout::Rect, style::Color};

// Create an EffectManager with &'static str keys
let mut manager = EffectManager::<&'static str>::default();

// Add effects
manager.add_effect(some_effect);
manager.add_unique_effect("popup", another_effect);  // Readable string ID

// Process effects each frame
manager.process_effects(duration, &mut buffer, area);

Key characteristics of the EffectManager:

  • Generic Key Type: Use any type that implements Clone + Ord + ThreadSafetyMarker
    • &'static str provides readable IDs with zero runtime overhead
    • Simple numeric types like u8 or u16 work but are less readable
    • Enums provide the best type safety and maintainability for complex applications
  • Automatic Cleanup: Completed effects are automatically removed
  • Unique Effects: Effects with the same key replace previous ones
  • Frame Processing: Call process_effects once per frame

Unique effect IDs and conflict prevention

Unique effect IDs prevent multiple effects from interfering with each other:

// Without unique IDs - effects can conflict
manager.add_effect(fade_effect_1);
manager.add_effect(fade_effect_2); // Both effects run simultaneously

// With unique IDs - effects replace each other
manager.add_unique_effect("fade", fade_effect_1);
manager.add_unique_effect("fade", fade_effect_2); // Replaces fade_effect_1

The registry pattern for centralized effect management

A registry wraps the EffectManager and provides application-specific functionality:

// Simple example with &'static str IDs
pub struct MyEffectRegistry {
    effects: EffectManager<&'static str>,
    // Additional state and functionality
}

impl MyEffectRegistry {
    pub fn show_popup(&mut self, area: Rect) {
        let effect = create_popup_effect(area);
        self.effects.add_unique_effect("popup", effect);  // Readable ID
    }
    
    pub fn hide_popup(&mut self) {
        let effect = create_fade_out_effect();
        self.effects.add_unique_effect("popup", effect);  // Same ID replaces previous
    }
}

// Note: For production code, you'll want to use enums for better type safety
// (shown in section 3: Building Your EffectRegistry Structure)

Dynamic area tracking with RefRect

RefRect enables effects to adapt to changing UI layouts:

use tachyonfx::RefRect;

// Create a shared area reference
let popup_area = RefRect::new(initial_rect);

// UI component updates the area
popup_area.set(new_rect);

// Effect automatically uses the updated area
let effect = fx::dynamic_area(popup_area, fx::fade_from(Color::Black, Color::Black, Duration::from_millis(200)));

3. Building Your EffectRegistry Structure

Now that you understand the basics with simple numeric IDs, let's build a production-ready registry using enums for better type safety and maintainability.

Defining custom effect identifiers (enum-based approach)

While &'static str is great for examples and prototyping, enums provide much better type safety, compile-time guarantees, and self-documenting code:

#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum EffectId {
    #[default]
    None,
    // UI Component Effects
    MainMenu,
    StatusBar,
    HelpPanel,
    
    // Popup Effects
    ConfirmDialog,
    ErrorModal,
    SettingsPanel,
    
    // Notification Effects
    SuccessNotification,
    WarningNotification,
    ErrorNotification,
    
    // Loading Effects
    LoadingSpinner,
    ProgressBar,
    
    // Background Effects
    BackgroundGlow,
    Particles,
}

Registry struct composition with EffectManager

Build your registry around the EffectManager:

use std::sync::mpsc::Sender;
use tachyonfx::{EffectManager, RefRect, Duration, Effect, fx, Motion};
use ratatui::{buffer::Buffer, layout::Rect, style::Color};

pub struct EffectRegistry {
    // Core effect management
    effects: EffectManager<EffectId>,
    
    // Application integration
    event_sender: Sender<AppEvent>,
    
    // Screen tracking
    screen_area: RefRect,
    
    // Registry state
    last_frame_time: Duration,
}

impl EffectRegistry {
    pub fn new(event_sender: Sender<AppEvent>) -> Self {
        Self {
            effects: EffectManager::default(),
            event_sender,
            screen_area: RefRect::default(),
            last_frame_time: Duration::ZERO,
        }
    }
    
    pub fn process_effects(&mut self, duration: Duration, buf: &mut Buffer, area: Rect) {
        self.last_frame_time = duration;
        self.effects.process_effects(duration, buf, area);
    }
    
    pub fn update_screen_area(&mut self, area: Rect) {
        self.screen_area.set(area);
    }
}

Integration with application events

Connect your registry to your application's event system:

#[derive(Debug, Clone)]
pub enum AppEvent {
    ShowHelp,
    HideHelp,
    ShowError(String),
    DismissError,
    StartLoading,
    StopLoading,
    // ... other events
}

impl EffectRegistry {
    pub fn handle_event(&mut self, event: &AppEvent) {
        match event {
            AppEvent::ShowHelp => self.show_help_panel(),
            AppEvent::HideHelp => self.hide_help_panel(),
            AppEvent::ShowError(msg) => self.show_error_modal(msg.clone()),
            AppEvent::DismissError => self.dismiss_error_modal(),
            AppEvent::StartLoading => self.start_loading_animation(),
            AppEvent::StopLoading => self.stop_loading_animation(),
        }
    }
}

4. Effect Registration Patterns

Unique effect registration with add_unique_effect

Use unique effects when you want to prevent conflicts:

impl EffectRegistry {
    pub fn show_modal(&mut self, title: String, content: String, area: Rect) {
        let effect = fx::sequence(&[
            fx::fade_to(Color::Rgb(64, 64, 64), Color::Rgb(64, 64, 64), 300),
            // Modal appearance
            fx::fx::parallel(&[
                fx::fade_from(200),
                scale_in(0.8, 1.0, 250),
            ]),
        ]);
        
        // Replace any existing modal
        self.effects.add_unique_effect(EffectId::Modal, effect);
    }
    
    pub fn hide_modal(&mut self) {
        let effect = fx::fx::sequence(&[
            fx::fx::parallel(&[
                fx::fade_to(Color::Black, Color::Black, 200),
                scale_out(1.0, 0.8, 200),
            ]),
        ]);
        
        self.effects.add_unique_effect(EffectId::Modal, effect);
    }
}

Regular effect registration with add_effect

Use regular effects for one-off animations that don't conflict:

impl EffectRegistry {
    pub fn flash_button(&mut self, button_area: Rect) {
        let effect = fx::fx::ping_pong(fx::sequence(&[
            fx::fade_to_fg(Color::Yellow, 100),
            fx::fx::sleep(25),
        ])).with_area(button_area);
        
        // Multiple flashes can happen simultaneously
        self.effects.add_effect(effect);
    }
    
    pub fn particle_burst(&mut self, origin: (u16, u16), count: usize) {
        for _ in 0..count {
            let effect = create_particle_effect(origin);
            self.effects.add_effect(effect);
        }
    }
}

Effect replacement and cancellation strategies

Implement sophisticated effect replacement:

impl EffectRegistry {
    pub fn transition_scene(&mut self, from_scene: SceneId, to_scene: SceneId) {
        // Cancel any existing scene transition
        self.effects.add_unique_effect(EffectId::SceneTransition, fx::never_complete(fx::fade_to(Color::Black, Color::Black, Duration::ZERO)));
        
        // Start new transition
        let transition = fx::sequence(&[
            // Fade out current scene
            fx::fade_to(Color::Black, Color::Black, Duration::from_millis(300)),
            // Dispatch scene change event
            self.dispatch_event(AppEvent::SceneChanged(to_scene)),
            // Fade in new scene
            fx::fade_from(Color::Black, Color::Black, Duration::from_millis(300)),
        ]);
        
        self.effects.add_unique_effect(EffectId::SceneTransition, transition);
    }
    
    pub fn interrupt_loading(&mut self) {
        // Replace loading animation with quick fade-out
        let quick_dismiss = fx::fade_to(Color::Black, Color::Black, Duration::from_millis(100));
        self.effects.add_unique_effect(EffectId::LoadingSpinner, quick_dismiss);
    }
}

Complex effect composition techniques

Build sophisticated effects using composition:

impl EffectRegistry {
    pub fn celebrate_success(&mut self, message: String) {
        let celebration = fx::sequence(&[
            // Stage 1: Burst effect
            fx::parallel(&[
                particle_burst_effect(self.screen_area.center(), 20),
                screen_flash(Color::Green, 150),
            ]),
            
            // Stage 2: Message display
            fx::parallel(&[
                success_message_effect(message),
                self.glow_effect(Color::Green, 0.5, Duration::from_millis(2000)),
            ]),
            
            // Stage 3: Cleanup
            fx::sequence(&[
                fx::sleep(3000),
                fx::fade_to(Color::Black, Color::Black, Duration::from_millis(500)),
                self.dispatch_event(AppEvent::CelebrationComplete),
            ]),
        ]);
        
        self.effects.add_unique_effect(EffectId::Celebration, celebration);
    }
    
    pub fn error_cascade(&mut self, errors: Vec<String>) {
        let mut effects = Vec::new();
        
        for (i, error) in errors.iter().enumerate() {
            let delay = Duration::from_millis(i as u64 * 200);
            let error_effect = fx::sequence(&[
                fx::sleep(delay),
                error_popup_effect(error.clone()),
            ]);
            effects.push(error_effect);
        }
        
        let cascade = parallel(&effects);
        self.effects.add_unique_effect(EffectId::ErrorCascade, cascade);
    }
}

5. Dynamic Area Management

Note: Tachyonfx provides the dynamic_area function in tachyonfx::fx which handles most of the functionality described in this section. The examples below show how to use it effectively.

The RefRect pattern for responsive effects

RefRect is a shared reference to a rectangle that can be updated from multiple places. The key insight is that RefRect updates happen in your widget's render method, and effects automatically use the updated area.

Here's the complete workflow:

1. Creating and sharing RefRect

use tachyonfx::{RefRect, fx, Motion};
use ratatui::style::Color;

#[derive(Clone)]
pub struct ResponsiveWidget {
    area: RefRect,
    content: String,
}

impl ResponsiveWidget {
    pub fn new(content: String) -> Self {
        Self {
            area: RefRect::default(),
            content,
        }
    }
    
    // This gives effects access to the shared area reference
    pub fn area_ref(&self) -> RefRect {
        self.area.clone()
    }
}

2. Updating RefRect during rendering (THIS IS THE KEY!)

impl Widget for ResponsiveWidget {
    fn render(self, area: Rect, buf: &mut Buffer) {
        // CRITICAL: Update the RefRect with the current render area
        // This happens every frame, so effects always have the latest area
        self.area.set(area);
        
        // Now render your widget content
        let block = Block::default().borders(Borders::ALL);
        block.render(area, buf);
    }
}

3. Effects automatically use the updated area

impl EffectRegistry {
    pub fn show_popup(&mut self, widget: &ResponsiveWidget) {
        // Get the shared area reference
        let area_ref = widget.area_ref();
        
        // Create effect using dynamic_area - it will automatically track updates
        let effect = fx::dynamic_area(area_ref, fx::sequence(&[
            fx::fade_from(Color::Black, Color::Black, Duration::from_millis(200)),
            self.pulse_effect(Color::Blue, Duration::from_millis(300)),
        ]));
        
        self.effects.add_unique_effect(EffectId::Popup, effect);
        
        // The effect will now automatically use the latest area
        // whenever the widget is re-rendered with a new size
    }
}

4. The complete flow

// 1. Widget creates RefRect and shares it
let mut popup_widget = ResponsiveWidget::new("Hello".to_string());
let area_ref = popup_widget.area_ref();

// 2. Effect is created using the shared RefRect
let effect = fx::dynamic_area(area_ref, fx::fade_from(Color::Black, Color::Black, Duration::from_millis(200)));
registry.effects.add_unique_effect(EffectId::Popup, effect);

// 3. During each frame:
//    a. Widget renders and updates RefRect
//    b. Effect automatically uses the updated area
terminal.draw(|frame| {
    let popup_area = calculate_popup_area(frame.size());
    
    // This call updates the RefRect inside the widget
    popup_widget.render(popup_area, frame.buffer_mut());
    
    // Effects process using the updated area
    registry.process_effects(frame_duration, frame.buffer_mut(), frame.size());
});

Using dynamic area effects

Tachyonfx provides the dynamic_area function to create effects that track area changes:

use tachyonfx::fx::dynamic_area;

// Create an effect that automatically adapts to area changes
let responsive_effect = fx::dynamic_area(area_ref, fx::fade_from(Color::Black, Color::Black, Duration::from_millis(200)));

// The effect will automatically use the current area from the RefRect
// whenever the area is updated

The dynamic_area function wraps any effect to make it responsive to area changes. When the RefRect is updated, the effect automatically uses the new area without needing to be recreated.

Handling window resizing and layout changes

Important: With RefRect, you usually don't need to manually handle resizing! The RefRect is automatically updated during the render cycle. However, here are the scenarios where you might need manual updates:

Automatic updates (most common case)

// This is the typical case - RefRect updates automatically
impl Widget for MyPopup {
    fn render(self, area: Rect, buf: &mut Buffer) {
        // RefRect is updated automatically every frame
        self.area_ref.set(area);
        
        // Effects using this RefRect will automatically use the new area
        // No manual resize handling needed!
        
        self.render_content(area, buf);
    }
}

Manual updates (special cases)

You only need manual RefRect updates in these specific situations:

  1. Screen-level RefRect updates (for full-screen effects):
impl EffectRegistry {
    pub fn handle_window_resize(&mut self, new_size: Rect) {
        // Update screen-level RefRect for full-screen effects
        self.screen_area.set(new_size);
        
        // Individual widget RefRects will be updated automatically
        // during their render methods
    }
}
  1. Calculating new positions outside the render cycle:
impl EffectRegistry {
    pub fn recalculate_modal_position(&mut self, screen_size: Rect) {
        // Only needed if you're calculating positions outside of render
        let modal_width = 60.min(screen_size.width - 4);
        let modal_height = 20.min(screen_size.height - 4);
        
        let new_modal_area = Rect {
            x: (screen_size.width - modal_width) / 2,
            y: (screen_size.height - modal_height) / 2,
            width: modal_width,
            height: modal_height,
        };
        
        // Update the RefRect directly
        self.modal_area.set(new_modal_area);
    }
}
  1. The typical application resize handler:
impl App {
    pub fn handle_resize(&mut self, new_size: Rect) {
        // Update screen-level RefRect
        self.effect_registry.screen_area.set(new_size);
        
        // Individual widget RefRects will be updated automatically
        // when their render methods are called in the next frame
        
        // Only manually update RefRects for effects that need immediate repositioning
        if self.has_active_modal() {
            self.effect_registry.recalculate_modal_position(new_size);
        }
    }
}

RefRect update timing summary

// ✅ AUTOMATIC - happens every frame
impl Widget for MyWidget {
    fn render(self, area: Rect, buf: &mut Buffer) {
        self.area_ref.set(area); // ← This updates RefRect automatically
        // ... render widget
    }
}

// ✅ MANUAL - only when needed for special cases
impl EffectRegistry {
    pub fn handle_special_case(&mut self) {
        // Only update RefRect manually when you need to change position
        // outside of the normal render cycle
        self.special_area.set(calculate_new_area());
    }
}

Area-based effect filtering

Use area-based filtering for precise effect control:

use tachyonfx::CellFilter;

impl EffectRegistry {
    pub fn dim_background_except(&mut self, exclude_area: RefRect) {
        let screen_filter = self.screen_area_filter();
        let exclude_filter = area_filter(exclude_area);
        
        let dimming_filter = CellFilter::AllOf(vec![
            screen_filter,
            CellFilter::Not(Box::new(exclude_filter)),
        ]);
        
        let dim_effect = fx::fade_to_fg(Color::Black, Duration::from_millis(300))
            .with_filter(dimming_filter);
        
        self.effects.add_unique_effect(EffectId::BackgroundDim, dim_effect);
    }
    
    pub fn highlight_area(&mut self, area: RefRect, color: Color) {
        let highlight_filter = area_filter(area);
        let highlight_effect = fx::fade_to_fg(color, Duration::from_millis(200))
            .with_filter(highlight_filter);
        
        self.effects.add_effect(highlight_effect);
    }
    
    fn screen_area_filter(&self) -> CellFilter {
        area_filter(self.screen_area.clone())
    }
}

fn area_filter(area: RefRect) -> CellFilter {
    CellFilter::PositionFn(Box::new(move |pos| {
        area.get().contains(pos)
    }))
}

6. Event-Driven Effect System

Connecting application events to effects

Create a clean mapping between events and effects:

#[derive(Debug, Clone)]
pub enum AppEvent {
    // UI Events
    ButtonClicked(String),
    MenuItemSelected(usize),
    InputFocused(String),
    InputBlurred(String),
    
    // System Events
    LoadingStarted,
    LoadingCompleted,
    ErrorOccurred(String),
    WarningIssued(String),
    
    // Navigation Events
    SceneChanged(SceneId),
    ModalOpened(ModalType),
    ModalClosed(ModalType),
}

impl EffectRegistry {
    pub fn handle_event(&mut self, event: &AppEvent) {
        match event {
            AppEvent::ButtonClicked(id) => self.animate_button_click(id),
            AppEvent::MenuItemSelected(index) => self.highlight_menu_item(*index),
            AppEvent::InputFocused(id) => self.show_input_focus(id),
            AppEvent::InputBlurred(id) => self.hide_input_focus(id),
            
            AppEvent::LoadingStarted => self.start_loading_animation(),
            AppEvent::LoadingCompleted => self.stop_loading_animation(),
            AppEvent::ErrorOccurred(msg) => self.show_error_notification(msg),
            AppEvent::WarningIssued(msg) => self.show_warning_notification(msg),
            
            AppEvent::SceneChanged(scene) => self.transition_to_scene(*scene),
            AppEvent::ModalOpened(modal_type) => self.show_modal(*modal_type),
            AppEvent::ModalClosed(modal_type) => self.hide_modal(*modal_type),
        }
    }
}

Event dispatch from effects back to application

Enable effects to communicate back to the application:

impl EffectRegistry {
    pub fn show_timed_notification(&mut self, message: String, duration: Duration) {
        let notification_effect = fx::sequence(&[
            // Show notification
            fx::fade_from(Color::Black, Color::Black, Duration::from_millis(200)),
            
            // Wait for specified duration
            fx::sleep(duration),
            
            // Hide notification and dispatch completion event
            fx::parallel(&[
                fx::fade_to(Color::Black, Color::Black, Duration::from_millis(200)),
                self.dispatch_event(AppEvent::NotificationDismissed),
            ]),
        ]);
        
        self.effects.add_unique_effect(EffectId::Notification, notification_effect);
    }
    
    pub fn show_confirm_dialog(&mut self, message: String, callback: ConfirmCallback) {
        let dialog_effect = fx::sequence(&[
            // Show dialog
            modal_open_effect(),
            
            // Wait for user interaction (this would be handled by input system)
            // The effect completes when user responds
            
            // Dispatch result
            self.dispatch_event(AppEvent::ConfirmResult(callback)),
        ]);
        
        self.effects.add_unique_effect(EffectId::ConfirmDialog, dialog_effect);
    }
    
    fn dispatch_event(&self, event: AppEvent) -> Effect {
        let sender = self.event_sender.clone();
        fx::effect_fn(event, Duration::ZERO, move |event, _ctx, _cells| {
            if let Some(event) = event.take() {
                let _ = sender.send(event);
            }
        })
    }
}

Lifecycle management and cleanup

Implement proper lifecycle management:

impl EffectRegistry {
    pub fn cleanup(&mut self) {
        // Force completion of all effects
        self.effects = EffectManager::default();
        
        // Reset state
        self.last_frame_time = Duration::ZERO;
        
        // Notify application of cleanup
        let _ = self.event_sender.send(AppEvent::EffectsCleanedUp);
    }
    
    pub fn pause_effects(&mut self) {
        // Implementation depends on your needs
        // You might want to store current effects and replace with static versions
    }
    
    pub fn resume_effects(&mut self) {
        // Restore paused effects
    }
    
    pub fn is_effect_active(&self, id: EffectId) -> bool {
        // This would require extending EffectManager to track active IDs
        // For now, you can maintain your own tracking
        self.active_effects.contains(&id)
    }
}

Error handling and recovery

Implement robust error handling:

use std::fmt;

#[derive(Debug)]
pub enum EffectError {
    EventDispatchFailed,
    InvalidArea,
    EffectCreationFailed,
    ResourceNotFound,
}

impl fmt::Display for EffectError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            EffectError::EventDispatchFailed => write!(f, "Failed to dispatch event"),
            EffectError::InvalidArea => write!(f, "Invalid area specified"),
            EffectError::EffectCreationFailed => write!(f, "Failed to create effect"),
            EffectError::ResourceNotFound => write!(f, "Required resource not found"),
        }
    }
}

impl EffectRegistry {
    pub fn try_show_modal(&mut self, modal_type: ModalType) -> Result<(), EffectError> {
        let area = self.calculate_modal_area(modal_type)
            .ok_or(EffectError::InvalidArea)?;
        
        let effect = self.create_modal_effect(modal_type, area)
            .ok_or(EffectError::EffectCreationFailed)?;
        
        self.effects.add_unique_effect(EffectId::Modal, effect);
        Ok(())
    }
    
    pub fn handle_error(&mut self, error: EffectError) {
        // Log the error
        eprintln!("Effect error: {}", error);
        
        // Show error notification
        self.show_error_notification(error.to_string());
        
        // Optionally dispatch to application
        let _ = self.event_sender.send(AppEvent::EffectError(error));
    }
}

7. Advanced Registry Features

Screen area tracking and management

Implement comprehensive screen area management:

#[derive(Clone)]
pub struct ScreenManager {
    current_area: RefRect,
    previous_area: RefRect,
    safe_area: RefRect,  // Area excluding borders/status bars
    resize_listeners: Vec<ResizeListener>,
}

impl ScreenManager {
    pub fn new() -> Self {
        Self {
            current_area: RefRect::default(),
            previous_area: RefRect::default(),
            safe_area: RefRect::default(),
            resize_listeners: Vec::new(),
        }
    }
    
    pub fn update_area(&mut self, new_area: Rect) {
        self.previous_area.set(self.current_area.get());
        self.current_area.set(new_area);
        
        // Calculate safe area (excluding borders)
        let safe_area = Rect {
            x: new_area.x + 1,
            y: new_area.y + 1,
            width: new_area.width.saturating_sub(2),
            height: new_area.height.saturating_sub(2),
        };
        self.safe_area.set(safe_area);
        
        // Notify listeners
        for listener in &self.resize_listeners {
            listener.on_resize(new_area);
        }
    }
    
    pub fn center_rect(&self, width: u16, height: u16) -> Rect {
        let area = self.current_area.get();
        Rect {
            x: area.x + (area.width.saturating_sub(width)) / 2,
            y: area.y + (area.height.saturating_sub(height)) / 2,
            width,
            height,
        }
    }
}

impl EffectRegistry {
    pub fn with_screen_manager(mut self, screen_manager: ScreenManager) -> Self {
        self.screen_manager = screen_manager;
        self
    }
    
    pub fn get_centered_area(&self, width: u16, height: u16) -> Rect {
        self.screen_manager.center_rect(width, height)
    }
    
    pub fn get_safe_area(&self) -> Rect {
        self.screen_manager.safe_area.get()
    }
}

Effect timing and duration control

Implement sophisticated timing controls:

#[derive(Clone)]
pub struct TimingController {
    speed_multiplier: f32,
    paused: bool,
    slow_motion_factor: f32,
}

impl TimingController {
    pub fn new() -> Self {
        Self {
            speed_multiplier: 1.0,
            paused: false,
            slow_motion_factor: 1.0,
        }
    }
    
    pub fn adjust_duration(&self, duration: Duration) -> Duration {
        if self.paused {
            return Duration::ZERO;
        }
        
        let multiplier = self.speed_multiplier * self.slow_motion_factor;
        Duration::from_millis((duration.as_millis() as f32 / multiplier) as u64)
    }
    
    pub fn set_speed(&mut self, multiplier: f32) {
        self.speed_multiplier = multiplier.max(0.1).min(10.0);
    }
    
    pub fn enable_slow_motion(&mut self, factor: f32) {
        self.slow_motion_factor = factor;
    }
    
    pub fn pause(&mut self) {
        self.paused = true;
    }
    
    pub fn resume(&mut self) {
        self.paused = false;
    }
}

impl EffectRegistry {
    pub fn process_effects_with_timing(&mut self, duration: Duration, buf: &mut Buffer, area: Rect) {
        let adjusted_duration = self.timing_controller.adjust_duration(duration);
        self.effects.process_effects(adjusted_duration, buf, area);
    }
    
    pub fn set_effect_speed(&mut self, speed: f32) {
        self.timing_controller.set_speed(speed);
    }
    
    pub fn enable_debug_slow_motion(&mut self) {
        self.timing_controller.enable_slow_motion(0.3);
    }
}

Complex effect sequencing and parallel execution

Build sophisticated effect orchestration:

impl EffectRegistry {
    pub fn orchestrate_scene_transition(&mut self, from: SceneId, to: SceneId) {
        let transition = fx::sequence(&[
            // Phase 1: Preparation
            fx::parallel(&[
                self.fade_out_ui_elements(),
                self.collect_particles_to_center(),
                self.dim_background(),
            ]),
            
            // Phase 2: Transition
            fx::sequence(&[
                self.dispatch_event(AppEvent::SceneTransitionStarted),
                fx::parallel(&[
                    self.particle_warp_effect(),
                    self.color_shift_effect(from.theme_color(), to.theme_color()),
                ]),
                fx::sleep(Duration::from_millis(200)),
                self.dispatch_event(AppEvent::SceneChanged(to)),
            ]),
            
            // Phase 3: Arrival
            fx::parallel(&[
                self.fade_in_ui_elements(),
                self.scatter_particles_from_center(),
                self.restore_background(),
            ]),
            
            // Phase 4: Completion
            fx::sequence(&[
                fx::sleep(Duration::from_millis(100)),
                self.dispatch_event(AppEvent::SceneTransitionCompleted),
            ]),
        ]);
        
        self.effects.add_unique_effect(EffectId::SceneTransition, transition);
    }
    
    pub fn cascade_loading_sequence(&mut self, elements: Vec<LoadingElement>) {
        let mut cascade_effects = Vec::new();
        
        for (i, element) in elements.into_iter().enumerate() {
            let delay = Duration::from_millis(i as u64 * 100);
            let element_effect = fx::sequence(&[
                fx::sleep(delay),
                self.create_loading_element_effect(element),
            ]);
            cascade_effects.push(element_effect);
        }
        
        let cascade = fx::sequence(&[
            // Start with background preparation
            self.prepare_loading_background(),
            
            // Run cascade
            parallel(&cascade_effects),
            
            // Complete loading
            fx::sequence(&[
                self.complete_loading_animation(),
                self.dispatch_event(AppEvent::LoadingCompleted),
            ]),
        ]);
        
        self.effects.add_unique_effect(EffectId::LoadingCascade, cascade);
    }
}

Custom effect utility functions

Create reusable effect utilities:

impl EffectRegistry {
    // Utility: Create a pulsing effect
    pub fn pulse_effect(&self, color: Color, duration: Duration) -> Effect {
        fx::fx::repeating(fx::fx::ping_pong(fx::fx::sequence(&[
            fx::fade_to_fg(color, duration / 4),
            fx::fade_to_fg(Color::Reset, duration / 4),
        ])))
    }
    
    // Utility: Create a typewriter effect
    pub fn typewriter_effect(&self, text: &str, char_delay: Duration) -> Effect {
        let mut effects = Vec::new();
        for (i, _) in text.chars().enumerate() {
            let delay = Duration::from_millis(i as u64 * char_delay.as_millis());
            effects.push(fx::sequence(&[
                fx::sleep(delay),
                reveal_text_up_to(i + 1),
            ]));
        }
        parallel(&effects)
    }
    
    // Utility: Create a bounce effect
    pub fn bounce_effect(&self, area: RefRect, intensity: f32) -> Effect {
        let bounce_sequence = fx::sequence(&[
            fx::translate(None, (-intensity as i16, 0), Duration::from_millis(150)),
            fx::translate(None, (intensity as i16, 0), Duration::from_millis(150)),
            fx::translate(None, ((-intensity * 0.6) as i16, 0), Duration::from_millis(100)),
            fx::translate(None, ((intensity * 0.6) as i16, 0), Duration::from_millis(100)),
            fx::translate(None, (0, 0), Duration::from_millis(100)),
        ]);
        
        // Use the built-in dynamic_area function from tachyonfx::fx
        fx::dynamic_area(area, bounce_sequence)
    }
    
    // Utility: Create a glow effect
    pub fn glow_effect(&self, color: Color, intensity: f32, duration: Duration) -> Effect {
        // Note: tachyonfx doesn't support alpha blending, using color directly
        fx::fx::repeating(fx::fx::ping_pong(fx::fx::sequence(&[
            fx::fade_to_fg(color, duration / 2),
            fx::fade_to_fg(Color::Reset, duration / 2),
        ])))
    }
    
    // Utility: Create a shake effect
    pub fn shake_effect(&self, intensity: f32, duration: Duration) -> Effect {
        let shake_duration = Duration::from_millis(50);
        let shake_count = (duration.as_millis() / shake_duration.as_millis()) as usize;
        
        let mut shakes = Vec::new();
        for _ in 0..shake_count {
            shakes.push(fx::sequence(&[
                fx::translate(None, (0, -intensity as i16), shake_duration / 2),
                fx::translate(None, (0, intensity as i16), shake_duration / 2),
            ]));
        }
        
        fx::sequence(&[
            parallel(&shakes),
            fx::translate(None, (0, 0), shake_duration), // Return to center
        ])
    }
}

8. Integration with Your Application

Render loop integration

Properly integrate the effect system with your render loop:

use ratatui::{
    backend::CrosstermBackend,
    Terminal,
    layout::Rect,
    Frame,
};
use std::time::{Duration, Instant};

pub struct App {
    effect_registry: EffectRegistry,
    last_frame: Instant,
    target_fps: u32,
    // ... other app state
}

impl App {
    pub fn new(event_sender: Sender<AppEvent>) -> Self {
        Self {
            effect_registry: EffectRegistry::new(event_sender),
            last_frame: Instant::now(),
            target_fps: 60,
        }
    }
    
    pub fn run(&mut self) -> Result<(), Box<dyn std::error::Error>> {
        let mut terminal = Terminal::new(CrosstermBackend::new(std::io::stdout()))?;
        
        loop {
            let frame_start = Instant::now();
            let frame_duration = frame_start.duration_since(self.last_frame);
            self.last_frame = frame_start;
            
            // Handle events
            if let Ok(event) = self.event_receiver.try_recv() {
                self.handle_event(event);
            }
            
            // Render
            terminal.draw(|f| self.render(f, frame_duration))?;
            
            // Frame rate limiting
            let frame_time = frame_start.elapsed();
            let target_frame_time = Duration::from_millis(1000 / self.target_fps as u64);
            if frame_time < target_frame_time {
                std::fx::sleep(target_frame_time - frame_time);
            }
        }
    }
    
    fn render(&mut self, frame: &mut Frame, frame_duration: Duration) {
        let area = frame.size();
        
        // Update effect registry with current screen size
        self.effect_registry.update_screen_area(area);
        
        // Render UI components first
        self.render_ui(frame, area);
        
        // Process and render effects on top
        let tachyon_duration = tachyonfx::Duration::from_std(frame_duration);
        self.effect_registry.process_effects(
            tachyon_duration,
            frame.buffer_mut(),
            area
        );
    }
    
    fn handle_event(&mut self, event: AppEvent) {
        // Handle app-specific logic
        match &event {
            AppEvent::Quit => self.should_quit = true,
            AppEvent::Resize(width, height) => {
                self.effect_registry.handle_resize(Rect::new(0, 0, *width, *height));
            }
            _ => {}
        }
        
        // Let effect registry handle the event
        self.effect_registry.handle_event(&event);
    }
}

Event processing workflow

Implement a comprehensive event processing system:

pub struct EventProcessor {
    event_queue: Vec<AppEvent>,
    effect_registry: EffectRegistry,
    ui_state: UiState,
}

impl EventProcessor {
    pub fn process_events(&mut self) {
        // Process events in order
        while let Some(event) = self.event_queue.pop() {
            self.process_single_event(event);
        }
    }
    
    fn process_single_event(&mut self, event: AppEvent) {
        // Pre-processing
        self.log_event(&event);
        
        // Update UI state
        self.ui_state.handle_event(&event);
        
        // Update effects
        self.effect_registry.handle_event(&event);
        
        // Handle side effects
        self.handle_side_effects(&event);
        
        // Post-processing
        self.cleanup_after_event(&event);
    }
    
    fn handle_side_effects(&mut self, event: &AppEvent) {
        match event {
            AppEvent::ModalClosed(_) => {
                // Clean up modal-related state
                self.cleanup_modal_state();
            }
            AppEvent::SceneChanged(scene) => {
                // Update navigation history
                self.update_navigation_history(*scene);
            }
            AppEvent::ErrorOccurred(error) => {
                // Log error and show notification
                self.log_error(error);
                self.show_error_notification(error.clone());
            }
            _ => {}
        }
    }
}

Performance considerations

Optimize your effect system for performance:

pub struct PerformanceMonitor {
    frame_times: Vec<Duration>,
    effect_processing_times: Vec<Duration>,
    max_history: usize,
}

impl PerformanceMonitor {
    pub fn new() -> Self {
        Self {
            frame_times: Vec::new(),
            effect_processing_times: Vec::new(),
            max_history: 100,
        }
    }
    
    pub fn record_frame_time(&mut self, duration: Duration) {
        self.frame_times.push(duration);
        if self.frame_times.len() > self.max_history {
            self.frame_times.remove(0);
        }
    }
    
    pub fn average_frame_time(&self) -> Duration {
        if self.frame_times.is_empty() {
            return Duration::ZERO;
        }
        
        let total: Duration = self.frame_times.iter().sum();
        total / self.frame_times.len() as u32
    }
    
    pub fn is_performance_degraded(&self) -> bool {
        self.average_frame_time() > Duration::from_millis(16) // 60 FPS threshold
    }
}

impl EffectRegistry {
    pub fn process_effects_with_monitoring(
        &mut self,
        duration: Duration,
        buf: &mut Buffer,
        area: Rect,
        monitor: &mut PerformanceMonitor,
    ) {
        let start = Instant::now();
        
        // Adjust effect quality based on performance
        if monitor.is_performance_degraded() {
            self.reduce_effect_quality();
        }
        
        self.effects.process_effects(duration, buf, area);
        
        monitor.record_frame_time(start.elapsed());
    }
    
    fn reduce_effect_quality(&mut self) {
        // Reduce particle counts, disable expensive effects, etc.
        self.timing_controller.set_speed(0.5); // Slow down effects
        self.disable_expensive_effects();
    }
}

Testing strategies

Implement comprehensive testing for your effect system:

#[cfg(test)]
mod tests {
    use super::*;
    use std::sync::mpsc;
    use ratatui::buffer::Buffer;
    use ratatui::layout::Rect;
    
    #[test]
    fn test_effect_registration() {
        let (sender, _receiver) = mpsc::channel();
        let mut registry = EffectRegistry::new(sender);
        
        // Test unique effect replacement
        registry.show_modal(ModalType::Confirm);
        assert!(registry.is_effect_active(EffectId::Modal));
        
        registry.show_modal(ModalType::Error);
        assert!(registry.is_effect_active(EffectId::Modal));
        // Should still be only one modal effect
    }
    
    #[test]
    fn test_area_updates() {
        let (sender, _receiver) = mpsc::channel();
        let mut registry = EffectRegistry::new(sender);
        
        let initial_area = Rect::new(0, 0, 80, 24);
        registry.update_screen_area(initial_area);
        
        let new_area = Rect::new(0, 0, 120, 30);
        registry.update_screen_area(new_area);
        
        assert_eq!(registry.get_screen_area(), new_area);
    }
    
    #[test]
    fn test_event_handling() {
        let (sender, receiver) = mpsc::channel();
        let mut registry = EffectRegistry::new(sender);
        
        registry.handle_event(&AppEvent::ShowError("Test error".to_string()));
        
        // Should dispatch a notification event
        let dispatched_event = receiver.try_recv().unwrap();
        assert!(matches!(dispatched_event, AppEvent::ErrorNotificationShown));
    }
    
    #[test]
    fn test_effect_completion() {
        let (sender, _receiver) = mpsc::channel();
        let mut registry = EffectRegistry::new(sender);
        
        // Add a short-duration effect
        registry.add_temporary_effect(Duration::from_millis(100));
        
        let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 10));
        let area = Rect::new(0, 0, 10, 10);
        
        // Process for longer than effect duration
        registry.process_effects(Duration::from_millis(200), &mut buffer, area);
        
        // Effect should be cleaned up
        assert_eq!(registry.active_effect_count(), 0);
    }
}

// Integration test helpers
pub struct EffectTestHarness {
    registry: EffectRegistry,
    buffer: Buffer,
    area: Rect,
    event_receiver: mpsc::Receiver<AppEvent>,
}

impl EffectTestHarness {
    pub fn new() -> Self {
        let (sender, receiver) = mpsc::channel();
        Self {
            registry: EffectRegistry::new(sender),
            buffer: Buffer::empty(Rect::new(0, 0, 80, 24)),
            area: Rect::new(0, 0, 80, 24),
            event_receiver: receiver,
        }
    }
    
    pub fn step(&mut self, duration: Duration) {
        let tachyon_duration = tachyonfx::Duration::from_std(duration);
        self.registry.process_effects(tachyon_duration, &mut self.buffer, self.area);
    }
    
    pub fn run_until_completion(&mut self, max_duration: Duration) {
        let step_duration = Duration::from_millis(16); // 60 FPS
        let mut total_time = Duration::ZERO;
        
        while total_time < max_duration && self.registry.has_active_effects() {
            self.step(step_duration);
            total_time += step_duration;
        }
    }
    
    pub fn assert_effect_active(&self, id: EffectId) {
        assert!(self.registry.is_effect_active(id), "Effect {:?} should be active", id);
    }
    
    pub fn assert_no_active_effects(&self) {
        assert_eq!(self.registry.active_effect_count(), 0, "No effects should be active");
    }
}

9. Common Patterns & Examples

Popup window effects with background dimming

Create sophisticated popup systems:

#[derive(Debug, Clone, Copy)]
pub enum PopupType {
    Confirm,
    Error,
    Info,
    Settings,
}

impl EffectRegistry {
    pub fn show_popup(&mut self, popup_type: PopupType, area: RefRect) {
        let popup_effect = self.create_popup_open_effect(popup_type, area.clone());
        let background_effect = self.create_background_dim_effect(area);
        
        let combined_effect = fx::parallel(&[
            popup_effect,
            background_effect,
        ]);
        
        self.effects.add_unique_effect(EffectId::Popup, combined_effect);
    }
    
    fn create_popup_open_effect(&self, popup_type: PopupType, area: RefRect) -> Effect {
        let open_effect = match popup_type {
            PopupType::Confirm => fx::sequence(&[
                scale_in(0.8, 1.0, Duration::from_millis(200)),
                self.shake_effect(1.0, Duration::from_millis(100)),
            ]),
            PopupType::Error => fx::sequence(&[
                fx::fade_to_fg(Color::Red, Duration::from_millis(100)),
                self.bounce_effect(area.clone(), 2.0),
                fx::fade_to_fg(Color::Reset, Duration::from_millis(100)),
            ]),
            PopupType::Info => fx::sequence(&[
                slide_in_from_top(Duration::from_millis(300)),
                self.glow_effect(Color::Blue, 0.3, Duration::from_millis(500)),
            ]),
            PopupType::Settings => fx::sequence(&[
                fade_in(Duration::from_millis(150)),
                slide_in_from_right(Duration::from_millis(200)),
            ]),
        };
        
        fx::dynamic_area(area, open_effect) // Using tachyonfx::fx::dynamic_area
    }
    
    fn create_background_dim_effect(&self, exclude_area: RefRect) -> Effect {
        let screen_area = self.screen_area.clone();
        let dim_filter = CellFilter::AllOf(vec![
            area_filter(screen_area),
            CellFilter::Not(Box::new(area_filter(exclude_area))),
        ]);
        
        fx::sequence(&[
            fx::sleep(Duration::from_millis(50)), // Slight delay
            fx::fade_to_fg(Color::DarkGray, Duration::from_millis(200)), // Using DarkGray as approximation for alpha
        ]).with_filter(dim_filter)
    }
    
    pub fn hide_popup(&mut self, popup_type: PopupType) {
        let hide_effect = match popup_type {
            PopupType::Confirm => scale_out(1.0, 0.8, Duration::from_millis(150)),
            PopupType::Error => fade_out(Duration::from_millis(100)),
            PopupType::Info => slide_out_to_top(Duration::from_millis(250)),
            PopupType::Settings => fx::parallel(&[
                fade_out(Duration::from_millis(150)),
                slide_out_to_right(Duration::from_millis(200)),
            ]),
        };
        
        let combined_effect = fx::parallel(&[
            hide_effect,
            fx::fade_to_fg(Color::Reset, Duration::from_millis(200)), // Remove background dim
        ]);
        
        self.effects.add_unique_effect(EffectId::Popup, combined_effect);
    }
}

Notification systems with complex animations

Build rich notification systems:

#[derive(Debug, Clone)]
pub struct NotificationData {
    pub message: String,
    pub level: NotificationLevel,
    pub duration: Duration,
    pub actions: Vec<NotificationAction>,
}

#[derive(Debug, Clone, Copy)]
pub enum NotificationLevel {
    Info,
    Warning,
    Error,
    Success,
}

impl EffectRegistry {
    pub fn show_notification(&mut self, notification: NotificationData) {
        let area = self.calculate_notification_area();
        let effect = self.create_notification_effect(notification, area);
        
        self.effects.add_unique_effect(EffectId::Notification, effect);
    }
    
    fn create_notification_effect(&self, notification: NotificationData, area: RefRect) -> Effect {
        let level_color = match notification.level {
            NotificationLevel::Info => Color::Blue,
            NotificationLevel::Warning => Color::Yellow,
            NotificationLevel::Error => Color::Red,
            NotificationLevel::Success => Color::Green,
        };
        
        let main_sequence = fx::sequence(&[
            // Stage 1: Slide in from top
            fx::sequence(&[
                slide_in_from_top(Duration::from_millis(300)),
                self.shake_effect(0.5, Duration::from_millis(50)), // Subtle impact
            ]),
            
            // Stage 2: Highlight based on level
            match notification.level {
                NotificationLevel::Error => fx::sequence(&[
                    self.pulse_effect(Color::Red, Duration::from_millis(200)),
                    self.pulse_effect(Color::Red, Duration::from_millis(200)),
                ]),
                NotificationLevel::Warning => self.pulse_effect(Color::Yellow, Duration::from_millis(300)),
                NotificationLevel::Success => self.glow_effect(Color::Green, 0.5, Duration::from_millis(500)),
                NotificationLevel::Info => fx::fade_to_fg(level_color, Duration::from_millis(100)),
            },
            
            // Stage 3: Display period
            fx::sequence(&[
                fx::sleep(notification.duration),
                // Subtle breathing effect during display
                fx::repeating(fx::sequence(&[
                    fx::fade_to_fg(level_color, Duration::from_millis(1000)), // Note: alpha not supported
                    fx::fade_to_fg(level_color, Duration::from_millis(1000)), // Note: alpha not supported
                ])),
            ]),
            
            // Stage 4: Dismissal
            fx::sequence(&[
                fade_out(Duration::from_millis(200)),
                slide_out_to_top(Duration::from_millis(150)),
            ]),
            
            // Stage 5: Cleanup
            self.dispatch_event(AppEvent::NotificationDismissed),
        ]);
        
        fx::dynamic_area(area, main_sequence)
    }
    
    pub fn show_notification_stack(&mut self, notifications: Vec<NotificationData>) {
        let base_area = self.calculate_notification_area();
        let mut stack_effects = Vec::new();
        
        for (i, notification) in notifications.into_iter().enumerate() {
            let offset_area = RefRect::new(Rect {
                y: base_area.get().y + i as u16 * 3,
                ..base_area.get()
            });
            
            let delay = Duration::from_millis(i as u64 * 100);
            let delayed_effect = fx::sequence(&[
                fx::sleep(delay),
                self.create_notification_effect(notification, offset_area),
            ]);
            
            stack_effects.push(delayed_effect);
        }
        
        let stack_effect = parallel(&stack_effects);
        self.effects.add_unique_effect(EffectId::NotificationStack, stack_effect);
    }
}

Loading indicators and progress effects

Implement dynamic loading systems:

#[derive(Debug, Clone)]
pub struct LoadingConfig {
    pub style: LoadingStyle,
    pub message: String,
    pub estimated_duration: Option<Duration>,
    pub cancellable: bool,
}

#[derive(Debug, Clone, Copy)]
pub enum LoadingStyle {
    Spinner,
    ProgressBar,
    Dots,
    Pulse,
    Wave,
}

impl EffectRegistry {
    pub fn start_loading(&mut self, config: LoadingConfig) {
        let area = self.calculate_loading_area();
        let effect = self.create_loading_effect(config, area);
        
        self.effects.add_unique_effect(EffectId::Loading, effect);
    }
    
    fn create_loading_effect(&self, config: LoadingConfig, area: RefRect) -> Effect {
        let loading_animation = match config.style {
            LoadingStyle::Spinner => self.create_spinner_effect(),
            LoadingStyle::ProgressBar => self.create_progress_bar_effect(config.estimated_duration),
            LoadingStyle::Dots => self.create_dots_effect(),
            LoadingStyle::Pulse => self.pulse_effect(Color::Blue, Duration::from_millis(500)),
            LoadingStyle::Wave => self.create_wave_effect(),
        };
        
        let main_effect = fx::sequence(&[
            // Setup
            fade_in(Duration::from_millis(200)),
            
            // Main loading animation
            loading_animation,
            
            // Cleanup (when loading completes)
            fade_out(Duration::from_millis(200)),
        ]);
        
        fx::dynamic_area(area, main_effect)
    }
    
    fn create_spinner_effect(&self) -> Effect {
        let spinner_chars = ['|', '/', '-', '\\'];
        let mut spinner_effects = Vec::new();
        
        for &ch in &spinner_chars {
            spinner_effects.push(fx::sequence(&[
                set_char(ch),
                fx::sleep(Duration::from_millis(125)),
            ]));
        }
        
        fx::repeating(sequence(&spinner_effects))
    }
    
    fn create_progress_bar_effect(&self, estimated_duration: Option<Duration>) -> Effect {
        if let Some(duration) = estimated_duration {
            // Animated progress bar
            let steps = 20;
            let step_duration = duration / steps;
            let mut progress_effects = Vec::new();
            
            for i in 0..=steps {
                let progress = i as f32 / steps as f32;
                progress_effects.push(fx::sequence(&[
                    set_progress_bar(progress),
                    fx::sleep(step_duration),
                ]));
            }
            
            sequence(&progress_effects)
        } else {
            // Indeterminate progress bar
            fx::repeating(fx::sequence(&[
                sweep_right(Duration::from_millis(800)),
                fx::sleep(Duration::from_millis(200)),
            ]))
        }
    }
    
    fn create_dots_effect(&self) -> Effect {
        fx::repeating(fx::sequence(&[
            typewriter_effect(".", Duration::from_millis(300)),
            typewriter_effect(".", Duration::from_millis(300)),
            typewriter_effect(".", Duration::from_millis(300)),
            clear_text(),
            fx::sleep(Duration::from_millis(200)),
        ]))
    }
    
    fn create_wave_effect(&self) -> Effect {
        let wave_chars = ['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'];
        let mut wave_effects = Vec::new();
        
        for (i, &ch) in wave_chars.iter().enumerate() {
            let delay = Duration::from_millis(i as u64 * 50);
            wave_effects.push(fx::sequence(&[
                fx::sleep(delay),
                set_char(ch),
                fx::sleep(Duration::from_millis(100)),
            ]));
        }
        
        fx::repeating(parallel(&wave_effects))
    }
    
    pub fn update_loading_progress(&mut self, progress: f32) {
        let progress_effect = set_progress_bar(progress.clamp(0.0, 1.0));
        self.effects.add_effect(progress_effect);
    }
    
    pub fn complete_loading(&mut self, success: bool) {
        let completion_effect = if success {
            fx::sequence(&[
                fx::fade_to_fg(Color::Green, Duration::from_millis(200)),
                show_checkmark(),
                fx::sleep(Duration::from_millis(500)),
                fade_out(Duration::from_millis(200)),
            ])
        } else {
            fx::sequence(&[
                fx::fade_to_fg(Color::Red, Duration::from_millis(200)),
                show_error_mark(),
                fx::sleep(Duration::from_millis(500)),
                fade_out(Duration::from_millis(200)),
            ])
        };
        
        self.effects.add_unique_effect(EffectId::Loading, completion_effect);
    }
}

Table/list interaction effects

Create interactive list/table effects:

#[derive(Debug, Clone)]
pub struct TableEffectConfig {
    pub row_areas: Vec<RefRect>,
    pub selected_row: Option<usize>,
    pub hover_row: Option<usize>,
}

impl EffectRegistry {
    pub fn update_table_selection(&mut self, old_selection: Option<usize>, new_selection: Option<usize>, config: &TableEffectConfig) {
        // Clear old selection
        if let Some(old_idx) = old_selection {
            if let Some(area) = config.row_areas.get(old_idx) {
                let deselect_effect = fx::sequence(&[
                    fx::fade_to_fg(Color::Reset, Duration::from_millis(150)),
                    remove_highlight(),
                ]);
                
                let effect = fx::dynamic_area(area.clone(), deselect_effect);
                self.effects.add_effect(effect);
            }
        }
        
        // Apply new selection
        if let Some(new_idx) = new_selection {
            if let Some(area) = config.row_areas.get(new_idx) {
                let select_effect = fx::sequence(&[
                    fx::fade_to_fg(Color::Blue, Duration::from_millis(100)),
                    self.pulse_effect(Color::Blue, Duration::from_millis(200)),
                    fx::fade_to_fg(Color::Blue, Duration::from_millis(100)), // Note: alpha not supported
                ]);
                
                let effect = fx::dynamic_area(area.clone(), select_effect);
                self.effects.add_unique_effect(EffectId::TableSelection, effect);
            }
        }
    }
    
    pub fn handle_table_hover(&mut self, row_index: Option<usize>, config: &TableEffectConfig) {
        if let Some(idx) = row_index {
            if let Some(area) = config.row_areas.get(idx) {
                let hover_effect = fx::sequence(&[
                    fx::fade_to_fg(Color::Gray, Duration::from_millis(100)),
                    self.glow_effect(Color::Gray, 0.3, Duration::from_millis(300)),
                ]);
                
                let effect = fx::dynamic_area(area.clone(), hover_effect);
                self.effects.add_unique_effect(EffectId::TableHover, effect);
            }
        } else {
            // Clear hover effect
            let clear_effect = fx::fade_to_fg(Color::Reset, Duration::from_millis(100));
            self.effects.add_unique_effect(EffectId::TableHover, clear_effect);
        }
    }
    
    pub fn animate_table_sort(&mut self, config: &TableEffectConfig) {
        let mut sort_effects = Vec::new();
        
        for (i, area) in config.row_areas.iter().enumerate() {
            let delay = Duration::from_millis(i as u64 * 50);
            let row_effect = fx::sequence(&[
                fx::sleep(delay),
                slide_out_to_left(Duration::from_millis(150)),
                fx::sleep(Duration::from_millis(100)),
                slide_in_from_left(Duration::from_millis(150)),
            ]);
            
            sort_effects.push(fx::dynamic_area(area.clone(), row_effect));
        }
        
        let sort_animation = parallel(&sort_effects);
        self.effects.add_effect(sort_animation);
    }
    
    pub fn highlight_table_data_change(&mut self, row_index: usize, config: &TableEffectConfig) {
        if let Some(area) = config.row_areas.get(row_index) {
            let highlight_effect = fx::sequence(&[
                fx::fade_to_fg(Color::Yellow, Duration::from_millis(100)),
                self.pulse_effect(Color::Yellow, Duration::from_millis(300)),
                fx::fade_to_fg(Color::Reset, Duration::from_millis(200)),
            ]);
            
            let effect = fx::dynamic_area(area.clone(), highlight_effect);
            self.effects.add_effect(effect);
        }
    }
    
    pub fn show_table_loading(&mut self, config: &TableEffectConfig) {
        let mut loading_effects = Vec::new();
        
        for (i, area) in config.row_areas.iter().enumerate() {
            let delay = Duration::from_millis(i as u64 * 100);
            let loading_effect = fx::sequence(&[
                fx::sleep(delay),
                fx::repeating(fx::sequence(&[
                    fx::fade_to_fg(Color::Gray, Duration::from_millis(500)),
                    fx::fade_to_fg(Color::DarkGray, Duration::from_millis(500)),
                ])),
            ]);
            
            loading_effects.push(fx::dynamic_area(area.clone(), loading_effect));
        }
        
        let table_loading = parallel(&loading_effects);
        self.effects.add_unique_effect(EffectId::TableLoading, table_loading);
    }
}

10. Best Practices & Tips

Effect naming conventions

Establish clear naming patterns for your effects:

// Good: Descriptive, action-oriented names
pub enum EffectId {
    // UI Component patterns: ComponentName[State]
    MainMenuExpanded,
    MainMenuCollapsed,
    StatusBarHighlight,
    
    // Action patterns: ActionTarget
    ButtonClickFeedback,
    InputValidationError,
    FormSubmissionSuccess,
    
    // State patterns: StateTransition
    LoadingToComplete,
    IdleToActive,
    
    // Modal patterns: ModalType[Action]
    ConfirmDialogOpen,
    ConfirmDialogClose,
    ErrorModalDismiss,
    
    // Background patterns: BackgroundType
    BackgroundDim,
    BackgroundParticles,
    BackgroundGlow,
}

// Bad: Vague or inconsistent names
pub enum BadEffectId {
    Effect1,
    Thing,
    Animation,
    Popup, // Too generic
    Red,   // Describes appearance, not purpose
}

Performance optimization

Optimize your effects for smooth performance:

impl EffectRegistry {
    // Optimization: Effect pooling
    pub fn create_pooled_effect(&mut self, effect_type: EffectType) -> Effect {
        if let Some(pooled) = self.effect_pool.get_mut(&effect_type) {
            pooled.reset();
            pooled.clone()
        } else {
            let new_effect = self.create_effect(effect_type);
            self.effect_pool.insert(effect_type, new_effect.clone());
            new_effect
        }
    }
    
    // Optimization: Batch similar effects
    pub fn batch_fade_effects(&mut self, fade_requests: Vec<FadeRequest>) {
        let mut batched_effects = Vec::new();
        
        for request in fade_requests {
            let effect = fx::fade_to_fg(request.color, request.duration)
                .with_filter(area_filter(request.area));
            batched_effects.push(effect);
        }
        
        let batch_effect = parallel(&batched_effects);
        self.effects.add_effect(batch_effect);
    }
    
    // Optimization: Adaptive quality
    pub fn set_performance_mode(&mut self, mode: PerformanceMode) {
        match mode {
            PerformanceMode::High => {
                self.particle_count = 100;
                self.animation_smoothness = 1.0;
                self.enable_expensive_effects = true;
            }
            PerformanceMode::Medium => {
                self.particle_count = 50;
                self.animation_smoothness = 0.8;
                self.enable_expensive_effects = true;
            }
            PerformanceMode::Low => {
                self.particle_count = 20;
                self.animation_smoothness = 0.6;
                self.enable_expensive_effects = false;
            }
        }
    }
    
    // Optimization: Early effect termination
    pub fn optimize_long_running_effects(&mut self) {
        // Skip frames for low-priority effects during high load
        if self.is_high_load() {
            self.timing_controller.set_skip_frames(true);
        }
    }
}

Debugging techniques

Implement debugging tools for your effect system:

#[derive(Debug, Clone)]
pub struct EffectDebugInfo {
    pub id: EffectId,
    pub start_time: Instant,
    pub duration: Duration,
    pub progress: f32,
    pub area: Rect,
}

impl EffectRegistry {
    pub fn enable_debug_mode(&mut self) {
        self.debug_mode = true;
        self.debug_overlay = true;
    }
    
    pub fn get_debug_info(&self) -> Vec<EffectDebugInfo> {
        // This would require extending EffectManager to track debug info
        self.active_effects.iter().map(|(id, effect)| {
            EffectDebugInfo {
                id: *id,
                start_time: effect.start_time(),
                duration: effect.duration(),
                progress: effect.progress(),
                area: effect.area().unwrap_or_default(),
            }
        }).collect()
    }
    
    pub fn render_debug_overlay(&self, buf: &mut Buffer, area: Rect) {
        if !self.debug_mode {
            return;
        }
        
        let debug_info = self.get_debug_info();
        let mut y = area.y;
        
        for info in debug_info {
            if y >= area.y + area.height {
                break;
            }
            
            let debug_text = format!(
                "{:?}: {:.1}% [{:.0}ms]",
                info.id,
                info.progress * 100.0,
                info.duration.as_millis()
            );
            
            buf.set_string(area.x, y, &debug_text, Style::default().fg(Color::Yellow));
            y += 1;
        }
    }
    
    pub fn log_effect_performance(&self, id: EffectId, processing_time: Duration) {
        if processing_time > Duration::from_millis(5) {
            eprintln!("Performance warning: Effect {:?} took {:?} to process", id, processing_time);
        }
    }
    
    pub fn dump_effect_state(&self) {
        println!("=== Effect Registry State ===");
        println!("Active effects: {}", self.active_effect_count());
        println!("Screen area: {:?}", self.screen_area.get());
        println!("Debug mode: {}", self.debug_mode);
        
        for (id, effect) in &self.active_effects {
            println!("  {:?}: {:?}", id, effect.debug_info());
        }
        println!("============================");
    }
}

Common pitfalls and solutions

Learn from common mistakes:

// Pitfall 1: Not cleaning up RefRect references
impl EffectRegistry {
    // Bad: Memory leak potential
    pub fn create_widget_effect_bad(&mut self, widget: &Widget) {
        let area = widget.area_ref(); // RefRect is cloned but never cleaned up
        let effect = fade_in(Duration::from_millis(200));
        self.effects.add_effect(fx::dynamic_area(area, effect));
        // If widget is destroyed, RefRect might persist
    }
    
    // Good: Proper lifecycle management
    pub fn create_widget_effect_good(&mut self, widget: &Widget) {
        let area = widget.area_ref();
        let effect = fx::sequence(&[
            fade_in(Duration::from_millis(200)),
            // Clean up when effect completes
            self.cleanup_widget_references(widget.id()),
        ]);
        self.effects.add_effect(fx::dynamic_area(area, effect));
    }
}

// Pitfall 2: Effect ID conflicts
impl EffectRegistry {
    // Bad: Generic IDs cause conflicts
    pub fn show_notification_bad(&mut self, message: String) {
        let effect = create_notification_effect(message);
        self.effects.add_unique_effect(EffectId::Notification, effect);
        // Multiple notifications will cancel each other
    }
    
    // Good: Specific IDs or use regular effects
    pub fn show_notification_good(&mut self, message: String, notification_id: NotificationId) {
        let effect = create_notification_effect(message);
        let effect_id = EffectId::Notification(notification_id);
        self.effects.add_unique_effect(effect_id, effect);
    }
}

// Pitfall 3: Ignoring frame timing
impl EffectRegistry {
    // Bad: Ignoring actual frame time
    pub fn process_effects_bad(&mut self, buf: &mut Buffer, area: Rect) {
        let fixed_duration = Duration::from_millis(16); // Always 16ms
        self.effects.process_effects(fixed_duration, buf, area);
        // Effects won't adapt to actual frame rate
    }
    
    // Good: Use actual frame timing
    pub fn process_effects_good(&mut self, frame_time: Duration, buf: &mut Buffer, area: Rect) {
        let clamped_time = frame_time.min(Duration::from_millis(33)); // Cap at 30 FPS
        self.effects.process_effects(clamped_time, buf, area);
    }
}

// Pitfall 4: Not handling edge cases
impl EffectRegistry {
    pub fn handle_window_resize(&mut self, new_size: Rect) {
        // Check for invalid sizes
        if new_size.width == 0 || new_size.height == 0 {
            return;
        }
        
        // Check for extreme sizes
        if new_size.width > 1000 || new_size.height > 1000 {
            eprintln!("Warning: Very large terminal size: {:?}", new_size);
        }
        
        // Update screen area
        self.screen_area.set(new_size);
        
        // Recalculate dependent areas
        self.recalculate_all_areas(new_size);
    }
}

11. Simple Example: Button with Click Effect

Let's start with a small, focused example that demonstrates the core EffectRegistry pattern:

use std::sync::mpsc::{self, Sender};
use tachyonfx::{EffectManager, Effect, RefRect, Duration, fx::*};
use ratatui::{buffer::Buffer, layout::Rect, style::Color, widgets::Widget};

// 1. Define your effect IDs
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
enum EffectId {
    ButtonClick,
    ButtonHover,
}

// 2. Define your application events
#[derive(Debug, Clone)]
enum AppEvent {
    ButtonClicked,
    ButtonHovered,
    ButtonUnhovered,
}

// 3. Create your effect registry
struct ButtonEffectRegistry {
    effects: EffectManager<EffectId>,
    event_sender: Sender<AppEvent>,
}

impl ButtonEffectRegistry {
    fn new(event_sender: Sender<AppEvent>) -> Self {
        Self {
            effects: EffectManager::default(),
            event_sender,
        }
    }

    // 4. Handle events and create effects
    fn handle_event(&mut self, event: &AppEvent, button_area: RefRect) {
        match event {
            AppEvent::ButtonClicked => {
                let click_effect = fx::sequence(&[
                    // Flash bright
                    fade_to(Color::Yellow, Duration::from_millis(100)),
                    // Pulse twice
                    fx::parallel(&[
                        fade_to(Color::White, Duration::from_millis(150)),
                        fade_to(Color::Reset, Duration::from_millis(150)),
                    ]),
                    // Return to normal
                    fade_to(Color::Reset, Duration::from_millis(100)),
                ]);
                
                let effect = fx::dynamic_area(button_area, click_effect);
                self.effects.add_unique_effect(EffectId::ButtonClick, effect);
            }
            
            AppEvent::ButtonHovered => {
                let hover_effect = fade_to(Color::Blue, Duration::from_millis(200));
                let effect = fx::dynamic_area(button_area, hover_effect);
                self.effects.add_unique_effect(EffectId::ButtonHover, effect);
            }
            
            AppEvent::ButtonUnhovered => {
                let unhover_effect = fade_to(Color::Reset, Duration::from_millis(200));
                let effect = fx::dynamic_area(button_area, unhover_effect);
                self.effects.add_unique_effect(EffectId::ButtonHover, effect);
            }
        }
    }

    // 5. Process effects each frame
    fn process_effects(&mut self, duration: Duration, buf: &mut Buffer, area: Rect) {
        self.effects.process_effects(duration, buf, area);
    }
}

// 6. Button widget that integrates with the registry
struct Button {
    text: String,
    area: RefRect,
    is_hovered: bool,
}

impl Button {
    fn new(text: String) -> Self {
        Self {
            text,
            area: RefRect::default(),
            is_hovered: false,
        }
    }
    
    fn area_ref(&self) -> RefRect {
        self.area.clone()
    }
    
    fn handle_click(&mut self, registry: &mut ButtonEffectRegistry) {
        registry.handle_event(&AppEvent::ButtonClicked, self.area.clone());
    }
    
    fn handle_hover(&mut self, registry: &mut ButtonEffectRegistry, hovered: bool) {
        if hovered && !self.is_hovered {
            registry.handle_event(&AppEvent::ButtonHovered, self.area.clone());
            self.is_hovered = true;
        } else if !hovered && self.is_hovered {
            registry.handle_event(&AppEvent::ButtonUnhovered, self.area.clone());
            self.is_hovered = false;
        }
    }
}

impl Widget for Button {
    fn render(self, area: Rect, buf: &mut Buffer) {
        // Update the RefRect - this is key for dynamic effects!
        self.area.set(area);
        
        // Render the button
        use ratatui::widgets::{Block, Borders, Paragraph};
        let button = Paragraph::new(self.text)
            .block(Block::default().borders(Borders::ALL))
            .alignment(ratatui::layout::Alignment::Center);
        
        button.render(area, buf);
    }
}

// 7. Usage in your application
fn example_usage() -> Result<(), Box<dyn std::error::Error>> {
    let (sender, _receiver) = mpsc::channel();
    let mut registry = ButtonEffectRegistry::new(sender);
    let mut button = Button::new("Click Me!".to_string());
    
    // In your event loop:
    loop {
        // Handle input events
        if let Some(mouse_event) = get_mouse_event()? {
            match mouse_event {
                MouseEvent::Click(pos) => {
                    if button.area_ref().get().contains(pos) {
                        button.handle_click(&mut registry);
                    }
                }
                MouseEvent::Move(pos) => {
                    let hovered = button.area_ref().get().contains(pos);
                    button.handle_hover(&mut registry, hovered);
                }
            }
        }
        
        // Render
        terminal.draw(|frame| {
            let button_area = centered_rect(20, 3, frame.size());
            
            // Render button (this updates the RefRect)
            button.render(button_area, frame.buffer_mut());
            
            // Process effects
            registry.process_effects(
                Duration::from_millis(16),
                frame.buffer_mut(),
                frame.size()
            );
        })?;
        
        if should_quit() { break; }
    }
    
    Ok(())
}

// Helper functions (implementation details omitted for brevity)
fn get_mouse_event() -> Result<Option<MouseEvent>, Box<dyn std::error::Error>> {
    // Implementation depends on your input handling
    Ok(None)
}

enum MouseEvent {
    Click((u16, u16)),
    Move((u16, u16)),
}

fn centered_rect(width: u16, height: u16, area: Rect) -> Rect {
    let x = (area.width.saturating_sub(width)) / 2;
    let y = (area.height.saturating_sub(height)) / 2;
    Rect::new(x, y, width, height)
}

fn should_quit() -> bool {
    // Implementation depends on your quit logic
    false
}

Key Points Demonstrated

  1. Effect IDs: Simple enum to identify different effect types
  2. Event Handling: Clean mapping from application events to effects
  3. RefRect Integration: Button updates its RefRect during render, effects track automatically
  4. Effect Composition: Click effect uses sequence and parallel for complex animation
  5. Unique Effects: Hover effects replace each other properly
  6. Dynamic Areas: Effects automatically adapt when button is resized/moved

This example shows the essential pattern without overwhelming complexity. The button can be clicked and hovered, with appropriate visual effects that automatically adapt to the button's current position and size.


12. Complete Working Example

Step-by-step implementation of a custom registry

Here's a complete implementation of a custom EffectRegistry for a sample application:

use std::sync::mpsc::{Sender, Receiver};
use std::collections::HashMap;
use std::time::Instant;

use tachyonfx::{
    EffectManager, Effect, Duration, RefRect, fx::*,
    CellFilter, Interpolation, Motion, IntoEffect,
};
// Note: dynamic_area is already available in tachyonfx::fx
use ratatui::{
    buffer::Buffer,
    layout::{Rect, Margin},
    style::Color,
};

// Step 1: Define your effect identifiers
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum MyEffectId {
    #[default]
    None,
    
    // UI Components
    MainMenu,
    Sidebar,
    StatusBar,
    
    // Popups
    ConfirmDialog,
    ErrorModal,
    SettingsPanel,
    
    // Notifications
    SuccessNotification,
    ErrorNotification,
    
    // Loading
    LoadingSpinner,
    
    // Background
    BackgroundDim,
    BackgroundParticles,
}

// Step 2: Define your application events
#[derive(Debug, Clone)]
pub enum AppEvent {
    ShowMainMenu,
    HideMainMenu,
    ShowSettings,
    HideSettings,
    ShowError(String),
    DismissError,
    ShowSuccess(String),
    StartLoading(String),
    StopLoading,
    WindowResized(Rect),
    Quit,
}

// Step 3: Create your registry structure
pub struct MyEffectRegistry {
    // Core components
    effects: EffectManager<MyEffectId>,
    event_sender: Sender<AppEvent>,
    
    // State tracking
    screen_area: RefRect,
    active_effects: HashMap<MyEffectId, Instant>,
    
    // Configuration
    debug_mode: bool,
    performance_mode: PerformanceMode,
    
    // UI Areas
    main_menu_area: RefRect,
    sidebar_area: RefRect,
    status_bar_area: RefRect,
    modal_area: RefRect,
}

#[derive(Debug, Clone, Copy)]
pub enum PerformanceMode {
    High,
    Medium,
    Low,
}

impl MyEffectRegistry {
    // Step 4: Implement constructor
    pub fn new(event_sender: Sender<AppEvent>) -> Self {
        Self {
            effects: EffectManager::default(),
            event_sender,
            screen_area: RefRect::default(),
            active_effects: HashMap::new(),
            debug_mode: false,
            performance_mode: PerformanceMode::High,
            main_menu_area: RefRect::default(),
            sidebar_area: RefRect::default(),
            status_bar_area: RefRect::default(),
            modal_area: RefRect::default(),
        }
    }
    
    // Step 5: Implement core functionality
    pub fn process_effects(&mut self, duration: Duration, buf: &mut Buffer, area: Rect) {
        // Update screen area
        self.screen_area.set(area);
        
        // Clean up tracking for completed effects
        self.active_effects.retain(|id, _| self.is_effect_still_active(*id));
        
        // Process effects
        self.effects.process_effects(duration, buf, area);
    }
    
    pub fn handle_event(&mut self, event: &AppEvent) {
        match event {
            AppEvent::ShowMainMenu => self.show_main_menu(),
            AppEvent::HideMainMenu => self.hide_main_menu(),
            AppEvent::ShowSettings => self.show_settings(),
            AppEvent::HideSettings => self.hide_settings(),
            AppEvent::ShowError(msg) => self.show_error(msg.clone()),
            AppEvent::DismissError => self.dismiss_error(),
            AppEvent::ShowSuccess(msg) => self.show_success(msg.clone()),
            AppEvent::StartLoading(msg) => self.start_loading(msg.clone()),
            AppEvent::StopLoading => self.stop_loading(),
            AppEvent::WindowResized(size) => self.handle_resize(*size),
            AppEvent::Quit => self.cleanup(),
        }
    }
    
    // Step 6: Implement effect methods
    pub fn show_main_menu(&mut self) {
        let menu_area = self.calculate_main_menu_area();
        self.main_menu_area.set(menu_area);
        
        let effect = fx::sequence(&[
            // Background setup
            fx::parallel(&[
                self.dim_background(),
                fx::sleep(Duration::from_millis(100)),
            ]),
            
            // Menu animation
            fx::dynamic_area(self.main_menu_area.clone(), fx::sequence(&[
                fade_in(Duration::from_millis(200)),
                slide_in_from_left(Duration::from_millis(300)),
                self.shake_effect(0.5, Duration::from_millis(50)),
            ])),
        ]);
        
        self.add_unique_effect(MyEffectId::MainMenu, effect);
    }
    
    pub fn hide_main_menu(&mut self) {
        let effect = fx::sequence(&[
            // Menu animation
            fx::dynamic_area(self.main_menu_area.clone(), fx::sequence(&[
                slide_out_to_left(Duration::from_millis(200)),
                fade_out(Duration::from_millis(100)),
            ])),
            
            // Background cleanup
            self.clear_background_dim(),
        ]);
        
        self.add_unique_effect(MyEffectId::MainMenu, effect);
    }
    
    pub fn show_error(&mut self, message: String) {
        let modal_area = self.calculate_modal_area();
        self.modal_area.set(modal_area);
        
        let effect = fx::sequence(&[
            // Background dim
            self.dim_background(),
            
            // Error modal
            fx::dynamic_area(self.modal_area.clone(), fx::sequence(&[
                // Dramatic entrance
                fx::parallel(&[
                    fx::fade_to_fg(Color::Red, Duration::from_millis(100)),
                    scale_in(0.5, 1.0, Duration::from_millis(200)),
                ]),
                
                // Error emphasis
                fx::sequence(&[
                    self.shake_effect(2.0, Duration::from_millis(200)),
                    self.pulse_effect(Color::Red, Duration::from_millis(300)),
                    self.pulse_effect(Color::Red, Duration::from_millis(300)),
                ]),
                
                // Settle to normal appearance
                fx::fade_to_fg(Color::Red, Duration::from_millis(200)), // Note: alpha not supported
            ])),
        ]);
        
        self.add_unique_effect(MyEffectId::ErrorModal, effect);
    }
    
    pub fn dismiss_error(&mut self) {
        let effect = fx::sequence(&[
            // Modal dismissal
            fx::dynamic_area(self.modal_area.clone(), fx::sequence(&[
                fade_out(Duration::from_millis(200)),
                scale_out(1.0, 0.8, Duration::from_millis(150)),
            ])),
            
            // Background cleanup
            self.clear_background_dim(),
            
            // Notify completion
            self.dispatch_event(AppEvent::DismissError),
        ]);
        
        self.add_unique_effect(MyEffectId::ErrorModal, effect);
    }
    
    pub fn show_success(&mut self, message: String) {
        let notification_area = self.calculate_notification_area();
        
        let effect = fx::sequence(&[
            // Success notification
            fx::dynamic_area(RefRect::new(notification_area), fx::sequence(&[
                // Slide in from top
                slide_in_from_top(Duration::from_millis(300)),
                
                // Success celebration
                fx::parallel(&[
                    fx::fade_to_fg(Color::Green, Duration::from_millis(100)),
                    self.glow_effect(Color::Green, 0.5, Duration::from_millis(500)),
                    particle_burst_effect(notification_area.center(), 10),
                ]),
                
                // Display period
                fx::sleep(Duration::from_millis(2000)),
                
                // Dismissal
                fx::sequence(&[
                    fade_out(Duration::from_millis(300)),
                    slide_out_to_top(Duration::from_millis(200)),
                ]),
            ])),
        ]);
        
        self.add_unique_effect(MyEffectId::SuccessNotification, effect);
    }
    
    pub fn start_loading(&mut self, message: String) {
        let loading_area = self.calculate_loading_area();
        
        let effect = fx::sequence(&[
            // Setup
            fade_in(Duration::from_millis(200)),
            
            // Loading animation
            fx::dynamic_area(RefRect::new(loading_area), 
                fx::repeating(self.create_spinner_effect())
            ),
        ]);
        
        self.add_unique_effect(MyEffectId::LoadingSpinner, effect);
    }
    
    pub fn stop_loading(&mut self) {
        let effect = fx::sequence(&[
            // Success indication
            fx::fade_to_fg(Color::Green, Duration::from_millis(100)),
            fx::sleep(Duration::from_millis(200)),
            
            // Dismissal
            fade_out(Duration::from_millis(200)),
            
            // Cleanup
            self.dispatch_event(AppEvent::StopLoading),
        ]);
        
        self.add_unique_effect(MyEffectId::LoadingSpinner, effect);
    }
    
    // Step 7: Implement utility methods
    fn add_unique_effect(&mut self, id: MyEffectId, effect: Effect) {
        self.active_effects.insert(id, Instant::now());
        self.effects.add_unique_effect(id, effect);
    }
    
    fn dim_background(&self) -> Effect {
        let screen_filter = area_filter(self.screen_area.clone());
        fx::fade_to_fg(Color::DarkGray, Duration::from_millis(300)) // Using DarkGray as approximation for alpha
            .with_filter(screen_filter)
    }
    
    fn clear_background_dim(&self) -> Effect {
        let screen_filter = area_filter(self.screen_area.clone());
        fx::fade_to_fg(Color::Reset, Duration::from_millis(200))
            .with_filter(screen_filter)
    }
    
    fn create_spinner_effect(&self) -> Effect {
        let chars = ['|', '/', '-', '\\'];
        let mut effects = Vec::new();
        
        for &ch in &chars {
            effects.push(fx::sequence(&[
                set_char_effect(ch),
                fx::sleep(Duration::from_millis(125)),
            ]));
        }
        
        sequence(&effects)
    }
    
    fn dispatch_event(&self, event: AppEvent) -> Effect {
        let sender = self.event_sender.clone();
        fx::effect_fn(event, Duration::ZERO, move |event, _ctx, _cells| {
            if let Some(event) = event.take() {
                let _ = sender.send(event);
            }
        })
    }
    
    fn particle_burst_effect(&self, center: (u16, u16), count: usize) -> Effect {
        // Implementation depends on your particle system
        // This is a placeholder
        fx::sequence(&[
            fx::fade_to_fg(Color::Yellow, Duration::from_millis(100)),
            fx::fade_to_fg(Color::Reset, Duration::from_millis(200)),
        ])
    }
    
    fn shake_effect(&self, intensity: f32, duration: Duration) -> Effect {
        // Implementation depends on your needs
        // This is a placeholder
        fx::sequence(&[
            fx::sleep(duration),
        ])
    }
    
    fn pulse_effect(&self, color: Color, duration: Duration) -> Effect {
        fx::sequence(&[
            fx::fade_to_fg(color, duration / 2),
            fx::fade_to_fg(color, duration / 2), // Note: alpha not supported
        ])
    }
    
    fn glow_effect(&self, color: Color, intensity: f32, duration: Duration) -> Effect {
        let glow_color = color.with_alpha(intensity);
        fx::repeating(fx::ping_pong(fx::sequence(&[
            fx::fade_to_fg(glow_color, duration / 2),
            fx::fade_to_fg(Color::Reset, duration / 2),
        ])))
    }
    
    // Step 8: Implement area calculations
    fn calculate_main_menu_area(&self) -> Rect {
        let screen = self.screen_area.get();
        Rect {
            x: screen.x,
            y: screen.y,
            width: screen.width / 3,
            height: screen.height,
        }
    }
    
    fn calculate_modal_area(&self) -> Rect {
        let screen = self.screen_area.get();
        let width = (screen.width * 2 / 3).min(60);
        let height = (screen.height * 2 / 3).min(20);
        
        Rect {
            x: screen.x + (screen.width - width) / 2,
            y: screen.y + (screen.height - height) / 2,
            width,
            height,
        }
    }
    
    fn calculate_notification_area(&self) -> Rect {
        let screen = self.screen_area.get();
        Rect {
            x: screen.x + screen.width / 4,
            y: screen.y + 2,
            width: screen.width / 2,
            height: 3,
        }
    }
    
    fn calculate_loading_area(&self) -> Rect {
        let screen = self.screen_area.get();
        Rect {
            x: screen.x + screen.width / 2 - 10,
            y: screen.y + screen.height / 2,
            width: 20,
            height: 3,
        }
    }
    
    // Step 9: Implement lifecycle methods
    fn handle_resize(&mut self, new_size: Rect) {
        self.screen_area.set(new_size);
        
        // Recalculate all areas
        self.main_menu_area.set(self.calculate_main_menu_area());
        self.modal_area.set(self.calculate_modal_area());
        
        // Restart any position-dependent effects
        self.restart_position_dependent_effects();
    }
    
    fn restart_position_dependent_effects(&mut self) {
        // Implementation depends on your specific effects
        if self.active_effects.contains_key(&MyEffectId::BackgroundParticles) {
            self.restart_background_particles();
        }
    }
    
    fn restart_background_particles(&mut self) {
        // Restart particle system with new screen size
        let particle_effect = self.create_background_particle_effect();
        self.add_unique_effect(MyEffectId::BackgroundParticles, particle_effect);
    }
    
    fn create_background_particle_effect(&self) -> Effect {
        // Placeholder implementation
        fx::repeating(fx::sequence(&[
            fx::fade_to_fg(Color::Blue, Duration::from_millis(1000)), // Note: alpha not supported
            fx::fade_to_fg(Color::Reset, Duration::from_millis(1000)),
        ]))
    }
    
    fn cleanup(&mut self) {
        self.active_effects.clear();
        self.effects = EffectManager::default();
    }
    
    fn is_effect_still_active(&self, id: MyEffectId) -> bool {
        // This would need to be implemented based on your EffectManager
        // For now, assume effects are cleaned up automatically
        true
    }
    
    // Step 10: Debug and utility methods
    pub fn enable_debug_mode(&mut self) {
        self.debug_mode = true;
    }
    
    pub fn set_performance_mode(&mut self, mode: PerformanceMode) {
        self.performance_mode = mode;
    }
    
    pub fn get_active_effects(&self) -> Vec<MyEffectId> {
        self.active_effects.keys().cloned().collect()
    }
    
    pub fn force_complete_effect(&mut self, id: MyEffectId) {
        // Force an effect to complete immediately
        let completion_effect = fade_out(Duration::from_millis(50));
        self.add_unique_effect(id, completion_effect);
    }
}

// Helper functions
fn area_filter(area: RefRect) -> CellFilter {
    CellFilter::PositionFn(Box::new(move |pos| {
        area.get().contains(pos)
    }))
}

fn set_char_effect(ch: char) -> Effect {
    fx::effect_fn(ch, Duration::ZERO, |ch, _ctx, cells| {
        if let Some(ch) = ch.take() {
            cells.for_each(|(_, cell)| {
                cell.set_char(ch);
            });
        }
    })
}

fn slide_in_from_left(duration: Duration) -> Effect {
    fx::slide_in(Motion::LeftToRight, 10, 0, Color::Black, duration)
}

fn slide_out_to_left(duration: Duration) -> Effect {
    fx::slide_out(Motion::RightToLeft, 10, 0, Color::Black, duration)
}

fn slide_in_from_top(duration: Duration) -> Effect {
    fx::slide_in(Motion::UpToDown, 10, 0, Color::Black, duration)
}

fn slide_out_to_top(duration: Duration) -> Effect {
    fx::slide_out(Motion::DownToUp, 10, 0, Color::Black, duration)
}

fn scale_in(from: f32, to: f32, duration: Duration) -> Effect {
    // Note: tachyonfx doesn't have direct scaling, use fade_from as approximation
    fx::fade_from(Color::Black, Color::Black, duration)
}

fn scale_out(from: f32, to: f32, duration: Duration) -> Effect {
    // Note: tachyonfx doesn't have direct scaling, use fade_to as approximation
    fx::fade_to(Color::Black, Color::Black, duration)
}

Sample application demonstrating all concepts

Here's a complete sample application that uses the registry:

use std::sync::mpsc;
use std::thread;
use std::time::{Duration, Instant};

use crossterm::{
    event::{self, Event, KeyCode},
    execute,
    terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{
    backend::CrosstermBackend,
    layout::{Constraint, Direction, Layout, Rect},
    style::{Color, Style},
    text::{Line, Span},
    widgets::{Block, Borders, Paragraph},
    Frame, Terminal,
};

// Sample application state
pub struct SampleApp {
    effect_registry: MyEffectRegistry,
    event_sender: mpsc::Sender<AppEvent>,
    event_receiver: mpsc::Receiver<AppEvent>,
    
    should_quit: bool,
    current_screen: Screen,
    input_buffer: String,
    
    last_frame: Instant,
}

#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Screen {
    Main,
    Settings,
    Loading,
}

impl SampleApp {
    pub fn new() -> Self {
        let (sender, receiver) = mpsc::channel();
        
        Self {
            effect_registry: MyEffectRegistry::new(sender.clone()),
            event_sender: sender,
            event_receiver: receiver,
            should_quit: false,
            current_screen: Screen::Main,
            input_buffer: String::new(),
            last_frame: Instant::now(),
        }
    }
    
    pub fn run(&mut self) -> Result<(), Box<dyn std::error::Error>> {
        // Setup terminal
        enable_raw_mode()?;
        let mut stdout = std::io::stdout();
        execute!(stdout, EnterAlternateScreen)?;
        let backend = CrosstermBackend::new(stdout);
        let mut terminal = Terminal::new(backend)?;
        
        // Run application loop
        let result = self.run_loop(&mut terminal);
        
        // Cleanup
        disable_raw_mode()?;
        execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
        terminal.show_cursor()?;
        
        result
    }
    
    fn run_loop(&mut self, terminal: &mut Terminal<CrosstermBackend<std::io::Stdout>>) -> Result<(), Box<dyn std::error::Error>> {
        // Show initial welcome effect
        self.event_sender.send(AppEvent::ShowSuccess("Welcome to the Sample App!".to_string()))?;
        
        loop {
            let frame_start = Instant::now();
            let frame_duration = frame_start.duration_since(self.last_frame);
            self.last_frame = frame_start;
            
            // Handle terminal events
            if event::poll(Duration::from_millis(0))? {
                if let Event::Key(key) = event::read()? {
                    self.handle_key_event(key.code)?;
                }
            }
            
            // Handle application events
            while let Ok(app_event) = self.event_receiver.try_recv() {
                self.handle_app_event(app_event)?;
            }
            
            // Render
            terminal.draw(|f| self.render(f, frame_duration))?;
            
            if self.should_quit {
                break;
            }
            
            // Frame rate limiting
            fx::sleep(Duration::from_millis(16)); // ~60 FPS
        }
        
        Ok(())
    }
    
    fn handle_key_event(&mut self, key: KeyCode) -> Result<(), Box<dyn std::error::Error>> {
        match key {
            KeyCode::Char('q') => {
                self.event_sender.send(AppEvent::Quit)?;
            }
            KeyCode::Char('m') => {
                self.event_sender.send(AppEvent::ShowMainMenu)?;
            }
            KeyCode::Char('s') => {
                self.event_sender.send(AppEvent::ShowSettings)?;
            }
            KeyCode::Char('e') => {
                self.event_sender.send(AppEvent::ShowError("This is a sample error message!".to_string()))?;
            }
            KeyCode::Char('l') => {
                self.event_sender.send(AppEvent::StartLoading("Loading something...".to_string()))?;
                
                // Simulate loading completion after 3 seconds
                let sender = self.event_sender.clone();
                thread::spawn(move || {
                    fx::sleep(Duration::from_secs(3));
                    let _ = sender.send(AppEvent::StopLoading);
                });
            }
            KeyCode::Esc => {
                // Dismiss current modal/menu
                match self.current_screen {
                    Screen::Settings => self.event_sender.send(AppEvent::HideSettings)?,
                    _ => self.event_sender.send(AppEvent::HideMainMenu)?,
                }
            }
            _ => {}
        }
        
        Ok(())
    }
    
    fn handle_app_event(&mut self, event: AppEvent) -> Result<(), Box<dyn std::error::Error>> {
        match &event {
            AppEvent::Quit => {
                self.should_quit = true;
            }
            AppEvent::ShowMainMenu => {
                self.current_screen = Screen::Main;
            }
            AppEvent::ShowSettings => {
                self.current_screen = Screen::Settings;
            }
            AppEvent::StartLoading(_) => {
                self.current_screen = Screen::Loading;
            }
            AppEvent::StopLoading => {
                self.current_screen = Screen::Main;
                self.event_sender.send(AppEvent::ShowSuccess("Loading completed!".to_string()))?;
            }
            _ => {}
        }
        
        // Let the effect registry handle the event
        self.effect_registry.handle_event(&event);
        
        Ok(())
    }
    
    fn render(&mut self, frame: &mut Frame, frame_duration: Duration) {
        let area = frame.size();
        
        // Render main UI
        self.render_ui(frame, area);
        
        // Process and render effects
        let tachyon_duration = tachyonfx::Duration::from_std(frame_duration);
        self.effect_registry.process_effects(tachyon_duration, frame.buffer_mut(), area);
        
        // Render help text
        self.render_help(frame, area);
    }
    
    fn render_ui(&self, frame: &mut Frame, area: Rect) {
        let main_block = Block::default()
            .title("Sample Effect Registry Application")
            .borders(Borders::ALL)
            .style(Style::default().fg(Color::White));
        
        frame.render_widget(main_block, area);
        
        let inner_area = area.inner(&ratatui::layout::Margin { horizontal: 1, vertical: 1 });
        
        match self.current_screen {
            Screen::Main => self.render_main_screen(frame, inner_area),
            Screen::Settings => self.render_settings_screen(frame, inner_area),
            Screen::Loading => self.render_loading_screen(frame, inner_area),
        }
    }
    
    fn render_main_screen(&self, frame: &mut Frame, area: Rect) {
        let welcome_text = Paragraph::new(vec![
            Line::from(vec![
                Span::styled("Welcome to the Sample App!", Style::default().fg(Color::Yellow)),
            ]),
            Line::from(""),
            Line::from("This demonstrates the EffectRegistry system."),
            Line::from(""),
            Line::from("Current screen: Main"),
        ]);
        
        frame.render_widget(welcome_text, area);
    }
    
    fn render_settings_screen(&self, frame: &mut Frame, area: Rect) {
        let settings_text = Paragraph::new(vec![
            Line::from(vec![
                Span::styled("Settings", Style::default().fg(Color::Green)),
            ]),
            Line::from(""),
            Line::from("This would be your settings screen."),
            Line::from(""),
            Line::from("Press ESC to go back."),
        ]);
        
        frame.render_widget(settings_text, area);
    }
    
    fn render_loading_screen(&self, frame: &mut Frame, area: Rect) {
        let loading_text = Paragraph::new(vec![
            Line::from(vec![
                Span::styled("Loading...", Style::default().fg(Color::Blue)),
            ]),
            Line::from(""),
            Line::from("Please wait while we load something."),
        ]);
        
        frame.render_widget(loading_text, area);
    }
    
    fn render_help(&self, frame: &mut Frame, area: Rect) {
        let help_area = Rect {
            x: area.x,
            y: area.y + area.height.saturating_sub(6),
            width: area.width,
            height: 6,
        };
        
        let help_text = Paragraph::new(vec![
            Line::from("Controls:"),
            Line::from("  q - Quit"),
            Line::from("  m - Show main menu"),
            Line::from("  s - Show settings"),
            Line::from("  e - Show error"),
            Line::from("  l - Start loading"),
            Line::from("  ESC - Dismiss current modal"),
        ])
        .block(Block::default().borders(Borders::TOP))
        .style(Style::default().fg(Color::Gray));
        
        frame.render_widget(help_text, help_area);
    }
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut app = SampleApp::new();
    app.run()
}

Testing and validation

Finally, here are comprehensive tests for the system:

#[cfg(test)]
mod tests {
    use super::*;
    use std::sync::mpsc;
    use std::time::Duration;
    
    #[test]
    fn test_effect_registry_creation() {
        let (sender, _receiver) = mpsc::channel();
        let registry = MyEffectRegistry::new(sender);
        
        assert_eq!(registry.get_active_effects().len(), 0);
        assert!(!registry.debug_mode);
    }
    
    #[test]
    fn test_unique_effect_replacement() {
        let (sender, _receiver) = mpsc::channel();
        let mut registry = MyEffectRegistry::new(sender);
        
        // Add first effect
        registry.show_main_menu();
        assert!(registry.get_active_effects().contains(&MyEffectId::MainMenu));
        
        // Add second effect with same ID - should replace first
        registry.show_main_menu();
        assert!(registry.get_active_effects().contains(&MyEffectId::MainMenu));
        
        // Should still have only one effect
        assert_eq!(registry.get_active_effects().len(), 1);
    }
    
    #[test]
    fn test_event_handling() {
        let (sender, receiver) = mpsc::channel();
        let mut registry = MyEffectRegistry::new(sender);
        
        // Test error event
        registry.handle_event(&AppEvent::ShowError("Test error".to_string()));
        assert!(registry.get_active_effects().contains(&MyEffectId::ErrorModal));
        
        // Test dismissal
        registry.handle_event(&AppEvent::DismissError);
        
        // Should have dispatched completion event
        let dispatched = receiver.try_recv();
        assert!(dispatched.is_ok());
    }
    
    #[test]
    fn test_area_calculations() {
        let (sender, _receiver) = mpsc::channel();
        let mut registry = MyEffectRegistry::new(sender);
        
        // Set screen size
        let screen_size = Rect::new(0, 0, 80, 24);
        registry.handle_event(&AppEvent::WindowResized(screen_size));
        
        // Test modal area calculation
        let modal_area = registry.calculate_modal_area();
        assert!(modal_area.width <= screen_size.width);
        assert!(modal_area.height <= screen_size.height);
        assert!(modal_area.x + modal_area.width <= screen_size.width);
        assert!(modal_area.y + modal_area.height <= screen_size.height);
    }
    
    #[test]
    fn test_performance_modes() {
        let (sender, _receiver) = mpsc::channel();
        let mut registry = MyEffectRegistry::new(sender);
        
        // Test different performance modes
        registry.set_performance_mode(PerformanceMode::Low);
        assert_eq!(registry.performance_mode, PerformanceMode::Low);
        
        registry.set_performance_mode(PerformanceMode::High);
        assert_eq!(registry.performance_mode, PerformanceMode::High);
    }
    
    #[test]
    fn test_cleanup() {
        let (sender, _receiver) = mpsc::channel();
        let mut registry = MyEffectRegistry::new(sender);
        
        // Add some effects
        registry.show_main_menu();
        registry.show_error("Test".to_string());
        assert!(!registry.get_active_effects().is_empty());
        
        // Cleanup
        registry.cleanup();
        assert!(registry.get_active_effects().is_empty());
    }
}

// Integration tests
#[cfg(test)]
mod integration_tests {
    use super::*;
    
    #[test]
    fn test_full_workflow() {
        let (sender, receiver) = mpsc::channel();
        let mut registry = MyEffectRegistry::new(sender);
        
        // Simulate a complete workflow
        registry.handle_event(&AppEvent::ShowMainMenu);
        registry.handle_event(&AppEvent::ShowSettings);
        registry.handle_event(&AppEvent::StartLoading("Test".to_string()));
        registry.handle_event(&AppEvent::StopLoading);
        registry.handle_event(&AppEvent::ShowSuccess("Done!".to_string()));
        
        // Verify events were processed
        assert!(!registry.get_active_effects().is_empty());
    }
    
    #[test]
    fn test_resize_handling() {
        let (sender, _receiver) = mpsc::channel();
        let mut registry = MyEffectRegistry::new(sender);
        
        // Initial size
        let size1 = Rect::new(0, 0, 80, 24);
        registry.handle_event(&AppEvent::WindowResized(size1));
        
        // Show modal
        registry.show_error("Test".to_string());
        
        // Resize
        let size2 = Rect::new(0, 0, 120, 30);
        registry.handle_event(&AppEvent::WindowResized(size2));
        
        // Effects should still be active and areas should be updated
        assert!(registry.get_active_effects().contains(&MyEffectId::ErrorModal));
    }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment