1use std::time::{Duration, Instant};
33
34use ratatui::{
35 buffer::Buffer,
36 layout::Rect,
37 style::{Color, Modifier, Style},
38 widgets::Widget,
39};
40use unicode_width::UnicodeWidthStr;
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
44pub enum AnimatedTextEffect {
45 #[default]
47 Pulse,
48 Wave,
50 Rainbow,
52 GradientShift,
54 Sparkle,
56}
57
58#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
60pub enum WaveDirection {
61 #[default]
63 Forward,
64 Backward,
66}
67
68#[derive(Debug, Clone)]
70pub struct AnimatedTextState {
71 pub frame: u8,
73 pub wave_position: usize,
75 pub wave_direction: WaveDirection,
77 last_tick: Option<Instant>,
79 interval: Duration,
81 pub active: bool,
83 sparkle_seed: u64,
85}
86
87impl Default for AnimatedTextState {
88 fn default() -> Self {
89 Self::new()
90 }
91}
92
93impl AnimatedTextState {
94 pub fn new() -> Self {
96 Self {
97 frame: 0,
98 wave_position: 0,
99 wave_direction: WaveDirection::Forward,
100 last_tick: None,
101 interval: Duration::from_millis(50),
102 active: true,
103 sparkle_seed: 0,
104 }
105 }
106
107 pub fn with_interval(interval_ms: u64) -> Self {
109 Self {
110 interval: Duration::from_millis(interval_ms),
111 ..Self::new()
112 }
113 }
114
115 pub fn set_interval(&mut self, interval_ms: u64) {
117 self.interval = Duration::from_millis(interval_ms);
118 }
119
120 pub fn tick(&mut self) -> bool {
124 self.tick_with_text_width(20) }
126
127 pub fn tick_with_text_width(&mut self, text_width: usize) -> bool {
131 if !self.active {
132 return false;
133 }
134
135 let now = Instant::now();
136
137 match self.last_tick {
138 Some(last) if now.duration_since(last) >= self.interval => {
139 self.frame = self.frame.wrapping_add(4);
141
142 let max_pos = text_width.saturating_sub(1);
144 match self.wave_direction {
145 WaveDirection::Forward => {
146 if self.wave_position >= max_pos {
147 self.wave_direction = WaveDirection::Backward;
148 } else {
149 self.wave_position += 1;
150 }
151 }
152 WaveDirection::Backward => {
153 if self.wave_position == 0 {
154 self.wave_direction = WaveDirection::Forward;
155 } else {
156 self.wave_position -= 1;
157 }
158 }
159 }
160
161 self.sparkle_seed = self.sparkle_seed.wrapping_add(1);
163
164 self.last_tick = Some(now);
165 true
166 }
167 None => {
168 self.last_tick = Some(now);
169 false
170 }
171 _ => false,
172 }
173 }
174
175 pub fn reset(&mut self) {
177 self.frame = 0;
178 self.wave_position = 0;
179 self.wave_direction = WaveDirection::Forward;
180 self.last_tick = None;
181 self.sparkle_seed = 0;
182 }
183
184 pub fn start(&mut self) {
186 self.active = true;
187 }
188
189 pub fn stop(&mut self) {
191 self.active = false;
192 }
193
194 pub fn is_active(&self) -> bool {
196 self.active
197 }
198
199 pub fn interpolation_factor(&self) -> f32 {
201 let radians = (self.frame as f32 / 255.0) * std::f32::consts::PI * 2.0;
203 (radians.sin() + 1.0) / 2.0
204 }
205}
206
207#[derive(Debug, Clone)]
209pub struct AnimatedTextStyle {
210 pub effect: AnimatedTextEffect,
212 pub primary_color: Color,
214 pub secondary_color: Color,
216 pub modifiers: Modifier,
218 pub wave_width: usize,
220 pub background: Option<Color>,
222 pub rainbow_colors: Vec<Color>,
224}
225
226impl Default for AnimatedTextStyle {
227 fn default() -> Self {
228 Self {
229 effect: AnimatedTextEffect::Pulse,
230 primary_color: Color::White,
231 secondary_color: Color::Cyan,
232 modifiers: Modifier::empty(),
233 wave_width: 3,
234 background: None,
235 rainbow_colors: vec![
236 Color::Red,
237 Color::Yellow,
238 Color::Green,
239 Color::Cyan,
240 Color::Blue,
241 Color::Magenta,
242 ],
243 }
244 }
245}
246
247impl AnimatedTextStyle {
248 pub fn new() -> Self {
250 Self::default()
251 }
252
253 pub fn pulse(primary: Color, secondary: Color) -> Self {
255 Self {
256 effect: AnimatedTextEffect::Pulse,
257 primary_color: primary,
258 secondary_color: secondary,
259 ..Default::default()
260 }
261 }
262
263 pub fn wave(base: Color, highlight: Color) -> Self {
265 Self {
266 effect: AnimatedTextEffect::Wave,
267 primary_color: base,
268 secondary_color: highlight,
269 wave_width: 3,
270 ..Default::default()
271 }
272 }
273
274 pub fn rainbow() -> Self {
276 Self {
277 effect: AnimatedTextEffect::Rainbow,
278 ..Default::default()
279 }
280 }
281
282 pub fn gradient_shift(start: Color, end: Color) -> Self {
284 Self {
285 effect: AnimatedTextEffect::GradientShift,
286 primary_color: start,
287 secondary_color: end,
288 ..Default::default()
289 }
290 }
291
292 pub fn sparkle(base: Color, sparkle: Color) -> Self {
294 Self {
295 effect: AnimatedTextEffect::Sparkle,
296 primary_color: base,
297 secondary_color: sparkle,
298 ..Default::default()
299 }
300 }
301
302 pub fn effect(mut self, effect: AnimatedTextEffect) -> Self {
304 self.effect = effect;
305 self
306 }
307
308 pub fn primary_color(mut self, color: Color) -> Self {
310 self.primary_color = color;
311 self
312 }
313
314 pub fn secondary_color(mut self, color: Color) -> Self {
316 self.secondary_color = color;
317 self
318 }
319
320 pub fn modifiers(mut self, modifiers: Modifier) -> Self {
322 self.modifiers = modifiers;
323 self
324 }
325
326 pub fn bold(mut self) -> Self {
328 self.modifiers = self.modifiers | Modifier::BOLD;
329 self
330 }
331
332 pub fn italic(mut self) -> Self {
334 self.modifiers = self.modifiers | Modifier::ITALIC;
335 self
336 }
337
338 pub fn wave_width(mut self, width: usize) -> Self {
340 self.wave_width = width.max(1);
341 self
342 }
343
344 pub fn background(mut self, color: Color) -> Self {
346 self.background = Some(color);
347 self
348 }
349
350 pub fn rainbow_colors(mut self, colors: Vec<Color>) -> Self {
352 if !colors.is_empty() {
353 self.rainbow_colors = colors;
354 }
355 self
356 }
357
358 pub fn success() -> Self {
362 Self::pulse(Color::Green, Color::LightGreen)
363 .bold()
364 }
365
366 pub fn warning() -> Self {
368 Self::pulse(Color::Yellow, Color::LightYellow)
369 .bold()
370 }
371
372 pub fn error() -> Self {
374 Self::pulse(Color::Red, Color::LightRed)
375 .bold()
376 }
377
378 pub fn info() -> Self {
380 Self::wave(Color::Blue, Color::Cyan)
381 }
382
383 pub fn loading() -> Self {
385 Self::wave(Color::DarkGray, Color::Cyan)
386 .wave_width(5)
387 }
388
389 pub fn highlight() -> Self {
391 Self::sparkle(Color::White, Color::Yellow)
392 }
393}
394
395#[derive(Debug, Clone)]
402pub struct AnimatedText<'a> {
403 text: &'a str,
405 state: &'a AnimatedTextState,
407 style: AnimatedTextStyle,
409}
410
411impl<'a> AnimatedText<'a> {
412 pub fn new(text: &'a str, state: &'a AnimatedTextState) -> Self {
414 Self {
415 text,
416 state,
417 style: AnimatedTextStyle::default(),
418 }
419 }
420
421 pub fn style(mut self, style: AnimatedTextStyle) -> Self {
423 self.style = style;
424 self
425 }
426
427 pub fn effect(mut self, effect: AnimatedTextEffect) -> Self {
429 self.style.effect = effect;
430 self
431 }
432
433 pub fn colors(mut self, primary: Color, secondary: Color) -> Self {
435 self.style.primary_color = primary;
436 self.style.secondary_color = secondary;
437 self
438 }
439
440 pub fn display_width(&self) -> usize {
442 self.text.width()
443 }
444
445 fn interpolate_color(c1: Color, c2: Color, factor: f32) -> Color {
447 match (c1, c2) {
448 (Color::Rgb(r1, g1, b1), Color::Rgb(r2, g2, b2)) => {
449 let r = (r1 as f32 + (r2 as f32 - r1 as f32) * factor) as u8;
450 let g = (g1 as f32 + (g2 as f32 - g1 as f32) * factor) as u8;
451 let b = (b1 as f32 + (b2 as f32 - b1 as f32) * factor) as u8;
452 Color::Rgb(r, g, b)
453 }
454 _ => {
455 if factor < 0.5 { c1 } else { c2 }
457 }
458 }
459 }
460
461 fn pulse_color(&self) -> Color {
463 let factor = self.state.interpolation_factor();
464 Self::interpolate_color(self.style.primary_color, self.style.secondary_color, factor)
465 }
466
467 fn wave_color(&self, char_index: usize) -> Color {
469 let wave_center = self.state.wave_position;
470 let half_width = self.style.wave_width / 2;
471 let start = wave_center.saturating_sub(half_width);
472 let end = wave_center + half_width + 1;
473
474 if char_index >= start && char_index < end {
475 let distance = if char_index >= wave_center {
477 char_index - wave_center
478 } else {
479 wave_center - char_index
480 };
481 let max_distance = half_width.max(1);
482 let intensity = 1.0 - (distance as f32 / max_distance as f32);
483 Self::interpolate_color(
484 self.style.primary_color,
485 self.style.secondary_color,
486 intensity,
487 )
488 } else {
489 self.style.primary_color
490 }
491 }
492
493 fn rainbow_color(&self, char_index: usize) -> Color {
495 let colors = &self.style.rainbow_colors;
496 if colors.is_empty() {
497 return self.style.primary_color;
498 }
499
500 let offset = (self.state.frame as usize) / 16;
502 let color_index = (char_index + offset) % colors.len();
503 colors[color_index]
504 }
505
506 fn gradient_color(&self, char_index: usize, text_width: usize) -> Color {
508 if text_width == 0 {
509 return self.style.primary_color;
510 }
511
512 let base_position = char_index as f32 / text_width.max(1) as f32;
514
515 let shift = self.state.frame as f32 / 255.0;
517 let position = (base_position + shift) % 1.0;
518
519 Self::interpolate_color(self.style.primary_color, self.style.secondary_color, position)
520 }
521
522 fn should_sparkle(&self, char_index: usize) -> bool {
524 let hash = char_index.wrapping_mul(31).wrapping_add(self.state.sparkle_seed as usize);
526 hash % 8 == 0 }
528
529 fn sparkle_color(&self, char_index: usize) -> Color {
531 if self.should_sparkle(char_index) {
532 self.style.secondary_color
533 } else {
534 self.style.primary_color
535 }
536 }
537}
538
539impl Widget for AnimatedText<'_> {
540 fn render(self, area: Rect, buf: &mut Buffer) {
541 if area.width == 0 || area.height == 0 {
542 return;
543 }
544
545 let text_width = self.text.width();
546 let mut x = area.x;
547 let y = area.y;
548
549 let base_style = Style::default()
551 .add_modifier(self.style.modifiers);
552
553 let base_style = if let Some(bg) = self.style.background {
554 base_style.bg(bg)
555 } else {
556 base_style
557 };
558
559 let mut char_index = 0;
561 for ch in self.text.chars() {
562 let ch_width = unicode_width::UnicodeWidthChar::width(ch).unwrap_or(0);
563
564 if x >= area.x + area.width {
565 break;
566 }
567
568 let fg_color = match self.style.effect {
570 AnimatedTextEffect::Pulse => self.pulse_color(),
571 AnimatedTextEffect::Wave => self.wave_color(char_index),
572 AnimatedTextEffect::Rainbow => self.rainbow_color(char_index),
573 AnimatedTextEffect::GradientShift => self.gradient_color(char_index, text_width),
574 AnimatedTextEffect::Sparkle => self.sparkle_color(char_index),
575 };
576
577 let style = base_style.fg(fg_color);
578
579 if x as usize + ch_width <= (area.x + area.width) as usize {
581 buf.set_string(x, y, ch.to_string(), style);
582 x += ch_width as u16;
583 }
584
585 char_index += 1;
586 }
587
588 while x < area.x + area.width {
590 buf.set_string(x, y, " ", base_style);
591 x += 1;
592 }
593 }
594}
595
596#[cfg(test)]
597mod tests {
598 use super::*;
599
600 #[test]
601 fn test_animated_text_state_new() {
602 let state = AnimatedTextState::new();
603 assert_eq!(state.frame, 0);
604 assert_eq!(state.wave_position, 0);
605 assert!(state.active);
606 }
607
608 #[test]
609 fn test_animated_text_state_with_interval() {
610 let state = AnimatedTextState::with_interval(100);
611 assert_eq!(state.interval, Duration::from_millis(100));
612 }
613
614 #[test]
615 fn test_animated_text_state_reset() {
616 let mut state = AnimatedTextState::new();
617 state.frame = 128;
618 state.wave_position = 10;
619 state.wave_direction = WaveDirection::Backward;
620
621 state.reset();
622
623 assert_eq!(state.frame, 0);
624 assert_eq!(state.wave_position, 0);
625 assert_eq!(state.wave_direction, WaveDirection::Forward);
626 }
627
628 #[test]
629 fn test_animated_text_state_start_stop() {
630 let mut state = AnimatedTextState::new();
631 assert!(state.is_active());
632
633 state.stop();
634 assert!(!state.is_active());
635
636 state.start();
637 assert!(state.is_active());
638 }
639
640 #[test]
641 fn test_animated_text_state_interpolation() {
642 let mut state = AnimatedTextState::new();
643
644 let factor = state.interpolation_factor();
646 assert!((factor - 0.5).abs() < 0.1);
647
648 state.frame = 64;
650 let factor = state.interpolation_factor();
651 assert!(factor > 0.8);
652
653 state.frame = 192;
655 let factor = state.interpolation_factor();
656 assert!(factor < 0.2);
657 }
658
659 #[test]
660 fn test_animated_text_style_presets() {
661 let pulse = AnimatedTextStyle::pulse(Color::Red, Color::Blue);
662 assert_eq!(pulse.effect, AnimatedTextEffect::Pulse);
663 assert_eq!(pulse.primary_color, Color::Red);
664 assert_eq!(pulse.secondary_color, Color::Blue);
665
666 let wave = AnimatedTextStyle::wave(Color::White, Color::Yellow);
667 assert_eq!(wave.effect, AnimatedTextEffect::Wave);
668
669 let rainbow = AnimatedTextStyle::rainbow();
670 assert_eq!(rainbow.effect, AnimatedTextEffect::Rainbow);
671 }
672
673 #[test]
674 fn test_animated_text_style_builder() {
675 let style = AnimatedTextStyle::new()
676 .effect(AnimatedTextEffect::Wave)
677 .primary_color(Color::Green)
678 .secondary_color(Color::Cyan)
679 .wave_width(5)
680 .bold();
681
682 assert_eq!(style.effect, AnimatedTextEffect::Wave);
683 assert_eq!(style.primary_color, Color::Green);
684 assert_eq!(style.secondary_color, Color::Cyan);
685 assert_eq!(style.wave_width, 5);
686 assert!(style.modifiers.contains(Modifier::BOLD));
687 }
688
689 #[test]
690 fn test_animated_text_style_presets_themed() {
691 let success = AnimatedTextStyle::success();
692 assert_eq!(success.primary_color, Color::Green);
693
694 let warning = AnimatedTextStyle::warning();
695 assert_eq!(warning.primary_color, Color::Yellow);
696
697 let error = AnimatedTextStyle::error();
698 assert_eq!(error.primary_color, Color::Red);
699
700 let info = AnimatedTextStyle::info();
701 assert_eq!(info.effect, AnimatedTextEffect::Wave);
702 }
703
704 #[test]
705 fn test_animated_text_display_width() {
706 let state = AnimatedTextState::new();
707 let text = AnimatedText::new("Hello", &state);
708 assert_eq!(text.display_width(), 5);
709
710 let text = AnimatedText::new("Hello World", &state);
711 assert_eq!(text.display_width(), 11);
712 }
713
714 #[test]
715 fn test_animated_text_render() {
716 let state = AnimatedTextState::new();
717 let text = AnimatedText::new("Test", &state);
718
719 let mut buf = Buffer::empty(Rect::new(0, 0, 10, 1));
720 text.render(Rect::new(0, 0, 10, 1), &mut buf);
721 }
723
724 #[test]
725 fn test_animated_text_render_wave() {
726 let mut state = AnimatedTextState::new();
727 state.wave_position = 2;
728
729 let style = AnimatedTextStyle::wave(Color::White, Color::Yellow);
730 let text = AnimatedText::new("Hello", &state).style(style);
731
732 let mut buf = Buffer::empty(Rect::new(0, 0, 10, 1));
733 text.render(Rect::new(0, 0, 10, 1), &mut buf);
734 }
736
737 #[test]
738 fn test_animated_text_render_rainbow() {
739 let state = AnimatedTextState::new();
740 let style = AnimatedTextStyle::rainbow();
741 let text = AnimatedText::new("Rainbow!", &state).style(style);
742
743 let mut buf = Buffer::empty(Rect::new(0, 0, 10, 1));
744 text.render(Rect::new(0, 0, 10, 1), &mut buf);
745 }
747
748 #[test]
749 fn test_animated_text_render_empty_area() {
750 let state = AnimatedTextState::new();
751 let text = AnimatedText::new("Test", &state);
752
753 let mut buf = Buffer::empty(Rect::new(0, 0, 0, 0));
754 text.render(Rect::new(0, 0, 0, 0), &mut buf);
755 }
757
758 #[test]
759 fn test_interpolate_color_rgb() {
760 let c1 = Color::Rgb(0, 0, 0);
762 let c2 = Color::Rgb(255, 255, 255);
763
764 let result = AnimatedText::interpolate_color(c1, c2, 0.5);
765 if let Color::Rgb(r, g, b) = result {
766 assert!((r as i16 - 127).abs() <= 1);
767 assert!((g as i16 - 127).abs() <= 1);
768 assert!((b as i16 - 127).abs() <= 1);
769 } else {
770 panic!("Expected RGB color");
771 }
772 }
773
774 #[test]
775 fn test_interpolate_color_non_rgb() {
776 let c1 = Color::Red;
778 let c2 = Color::Blue;
779
780 assert_eq!(AnimatedText::interpolate_color(c1, c2, 0.3), Color::Red);
781 assert_eq!(AnimatedText::interpolate_color(c1, c2, 0.7), Color::Blue);
782 }
783
784 #[test]
785 fn test_wave_direction_changes() {
786 let mut state = AnimatedTextState::new();
787 state.interval = Duration::from_millis(0); state.last_tick = Some(Instant::now() - Duration::from_secs(1));
789
790 let text_width = 10;
792 for _ in 0..15 {
793 state.tick_with_text_width(text_width);
794 }
795
796 assert_eq!(state.wave_direction, WaveDirection::Backward);
798 }
799}