A comprehensive guide to organizing and managing effects in terminal applications using tachyonfx's EffectManager with custom registry patterns.
- 1. Introduction & Overview
- 2. Core Concepts
- 3. Building Your EffectRegistry Structure
- 4. Effect Registration Patterns
- 5. Dynamic Area Management
- 6. Event-Driven Effect System
- 7. Advanced Registry Features
- 8. Integration with Your Application
- 9. Common Patterns & Examples
- 10. Best Practices & Tips
- 11. Complete Working Example
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
The registry pattern provides a clean abstraction layer between your application logic and the effect system:
- Separation of Concerns: Effect management is isolated from UI rendering logic
- Consistency: Standardized approach to effect registration and management
- Extensibility: Easy to add new effect types and behaviors
- Maintainability: Centralized location for all effect-related code
- Testing: Easier to test effect behavior in isolation
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.
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
oru16
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 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
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)
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)));
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.
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,
}
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);
}
}
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(),
}
}
}
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);
}
}
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);
}
}
}
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);
}
}
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);
}
}
Note: Tachyonfx provides the
dynamic_area
function intachyonfx::fx
which handles most of the functionality described in this section. The examples below show how to use it effectively.
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:
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()
}
}
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);
}
}
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
}
}
// 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());
});
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.
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:
// 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);
}
}
You only need manual RefRect updates in these specific situations:
- 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
}
}
- 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);
}
}
- 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);
}
}
}
// ✅ 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());
}
}
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)
}))
}
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),
}
}
}
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);
}
})
}
}
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)
}
}
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));
}
}
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()
}
}
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);
}
}
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);
}
}
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
])
}
}
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);
}
}
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());
}
_ => {}
}
}
}
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();
}
}
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");
}
}
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);
}
}
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);
}
}
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);
}
}
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);
}
}
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
}
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);
}
}
}
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!("============================");
}
}
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);
}
}
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
}
- Effect IDs: Simple enum to identify different effect types
- Event Handling: Clean mapping from application events to effects
- RefRect Integration: Button updates its RefRect during render, effects track automatically
- Effect Composition: Click effect uses sequence and parallel for complex animation
- Unique Effects: Hover effects replace each other properly
- 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.
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)
}
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()
}
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));
}
}