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 From<&crate::theme::Theme> for AnimatedTextStyle {
248 fn from(theme: &crate::theme::Theme) -> Self {
249 let p = &theme.palette;
250 Self {
251 effect: AnimatedTextEffect::Pulse,
252 primary_color: p.text,
253 secondary_color: p.secondary,
254 modifiers: Modifier::empty(),
255 wave_width: 3,
256 background: None,
257 rainbow_colors: vec![
258 Color::Red,
259 Color::Yellow,
260 Color::Green,
261 Color::Cyan,
262 Color::Blue,
263 Color::Magenta,
264 ],
265 }
266 }
267}
268
269impl AnimatedTextStyle {
270 pub fn new() -> Self {
272 Self::default()
273 }
274
275 pub fn pulse(primary: Color, secondary: Color) -> Self {
277 Self {
278 effect: AnimatedTextEffect::Pulse,
279 primary_color: primary,
280 secondary_color: secondary,
281 ..Default::default()
282 }
283 }
284
285 pub fn wave(base: Color, highlight: Color) -> Self {
287 Self {
288 effect: AnimatedTextEffect::Wave,
289 primary_color: base,
290 secondary_color: highlight,
291 wave_width: 3,
292 ..Default::default()
293 }
294 }
295
296 pub fn rainbow() -> Self {
298 Self {
299 effect: AnimatedTextEffect::Rainbow,
300 ..Default::default()
301 }
302 }
303
304 pub fn gradient_shift(start: Color, end: Color) -> Self {
306 Self {
307 effect: AnimatedTextEffect::GradientShift,
308 primary_color: start,
309 secondary_color: end,
310 ..Default::default()
311 }
312 }
313
314 pub fn sparkle(base: Color, sparkle: Color) -> Self {
316 Self {
317 effect: AnimatedTextEffect::Sparkle,
318 primary_color: base,
319 secondary_color: sparkle,
320 ..Default::default()
321 }
322 }
323
324 pub fn effect(mut self, effect: AnimatedTextEffect) -> Self {
326 self.effect = effect;
327 self
328 }
329
330 pub fn primary_color(mut self, color: Color) -> Self {
332 self.primary_color = color;
333 self
334 }
335
336 pub fn secondary_color(mut self, color: Color) -> Self {
338 self.secondary_color = color;
339 self
340 }
341
342 pub fn modifiers(mut self, modifiers: Modifier) -> Self {
344 self.modifiers = modifiers;
345 self
346 }
347
348 pub fn bold(mut self) -> Self {
350 self.modifiers |= Modifier::BOLD;
351 self
352 }
353
354 pub fn italic(mut self) -> Self {
356 self.modifiers |= Modifier::ITALIC;
357 self
358 }
359
360 pub fn wave_width(mut self, width: usize) -> Self {
362 self.wave_width = width.max(1);
363 self
364 }
365
366 pub fn background(mut self, color: Color) -> Self {
368 self.background = Some(color);
369 self
370 }
371
372 pub fn rainbow_colors(mut self, colors: Vec<Color>) -> Self {
374 if !colors.is_empty() {
375 self.rainbow_colors = colors;
376 }
377 self
378 }
379
380 pub fn success() -> Self {
384 Self::pulse(Color::Green, Color::LightGreen).bold()
385 }
386
387 pub fn warning() -> Self {
389 Self::pulse(Color::Yellow, Color::LightYellow).bold()
390 }
391
392 pub fn error() -> Self {
394 Self::pulse(Color::Red, Color::LightRed).bold()
395 }
396
397 pub fn info() -> Self {
399 Self::wave(Color::Blue, Color::Cyan)
400 }
401
402 pub fn loading() -> Self {
404 Self::wave(Color::DarkGray, Color::Cyan).wave_width(5)
405 }
406
407 pub fn highlight() -> Self {
409 Self::sparkle(Color::White, Color::Yellow)
410 }
411}
412
413#[derive(Debug, Clone)]
420pub struct AnimatedText<'a> {
421 text: &'a str,
423 state: &'a AnimatedTextState,
425 style: AnimatedTextStyle,
427}
428
429impl<'a> AnimatedText<'a> {
430 pub fn new(text: &'a str, state: &'a AnimatedTextState) -> Self {
432 Self {
433 text,
434 state,
435 style: AnimatedTextStyle::default(),
436 }
437 }
438
439 pub fn style(mut self, style: AnimatedTextStyle) -> Self {
441 self.style = style;
442 self
443 }
444
445 pub fn theme(self, theme: &crate::theme::Theme) -> Self {
447 self.style(AnimatedTextStyle::from(theme))
448 }
449
450 pub fn effect(mut self, effect: AnimatedTextEffect) -> Self {
452 self.style.effect = effect;
453 self
454 }
455
456 pub fn colors(mut self, primary: Color, secondary: Color) -> Self {
458 self.style.primary_color = primary;
459 self.style.secondary_color = secondary;
460 self
461 }
462
463 pub fn display_width(&self) -> usize {
465 self.text.width()
466 }
467
468 fn interpolate_color(c1: Color, c2: Color, factor: f32) -> Color {
470 match (c1, c2) {
471 (Color::Rgb(r1, g1, b1), Color::Rgb(r2, g2, b2)) => {
472 let r = (r1 as f32 + (r2 as f32 - r1 as f32) * factor) as u8;
473 let g = (g1 as f32 + (g2 as f32 - g1 as f32) * factor) as u8;
474 let b = (b1 as f32 + (b2 as f32 - b1 as f32) * factor) as u8;
475 Color::Rgb(r, g, b)
476 }
477 _ => {
478 if factor < 0.5 { c1 } else { c2 }
480 }
481 }
482 }
483
484 fn pulse_color(&self) -> Color {
486 let factor = self.state.interpolation_factor();
487 Self::interpolate_color(self.style.primary_color, self.style.secondary_color, factor)
488 }
489
490 fn wave_color(&self, char_index: usize) -> Color {
492 let wave_center = self.state.wave_position;
493 let half_width = self.style.wave_width / 2;
494 let start = wave_center.saturating_sub(half_width);
495 let end = wave_center + half_width + 1;
496
497 if char_index >= start && char_index < end {
498 let distance = char_index.abs_diff(wave_center);
500 let max_distance = half_width.max(1);
501 let intensity = 1.0 - (distance as f32 / max_distance as f32);
502 Self::interpolate_color(
503 self.style.primary_color,
504 self.style.secondary_color,
505 intensity,
506 )
507 } else {
508 self.style.primary_color
509 }
510 }
511
512 fn rainbow_color(&self, char_index: usize) -> Color {
514 let colors = &self.style.rainbow_colors;
515 if colors.is_empty() {
516 return self.style.primary_color;
517 }
518
519 let offset = (self.state.frame as usize) / 16;
521 let color_index = (char_index + offset) % colors.len();
522 colors[color_index]
523 }
524
525 fn gradient_color(&self, char_index: usize, text_width: usize) -> Color {
527 if text_width == 0 {
528 return self.style.primary_color;
529 }
530
531 let base_position = char_index as f32 / text_width.max(1) as f32;
533
534 let shift = self.state.frame as f32 / 255.0;
536 let position = (base_position + shift) % 1.0;
537
538 Self::interpolate_color(
539 self.style.primary_color,
540 self.style.secondary_color,
541 position,
542 )
543 }
544
545 fn should_sparkle(&self, char_index: usize) -> bool {
547 let hash = char_index
549 .wrapping_mul(31)
550 .wrapping_add(self.state.sparkle_seed as usize);
551 hash % 8 == 0 }
553
554 fn sparkle_color(&self, char_index: usize) -> Color {
556 if self.should_sparkle(char_index) {
557 self.style.secondary_color
558 } else {
559 self.style.primary_color
560 }
561 }
562}
563
564impl Widget for AnimatedText<'_> {
565 fn render(self, area: Rect, buf: &mut Buffer) {
566 if area.width == 0 || area.height == 0 {
567 return;
568 }
569
570 let text_width = self.text.width();
571 let mut x = area.x;
572 let y = area.y;
573
574 let base_style = Style::default().add_modifier(self.style.modifiers);
576
577 let base_style = if let Some(bg) = self.style.background {
578 base_style.bg(bg)
579 } else {
580 base_style
581 };
582
583 for (char_index, ch) in self.text.chars().enumerate() {
585 let ch_width = unicode_width::UnicodeWidthChar::width(ch).unwrap_or(0);
586
587 if x >= area.x + area.width {
588 break;
589 }
590
591 let fg_color = match self.style.effect {
593 AnimatedTextEffect::Pulse => self.pulse_color(),
594 AnimatedTextEffect::Wave => self.wave_color(char_index),
595 AnimatedTextEffect::Rainbow => self.rainbow_color(char_index),
596 AnimatedTextEffect::GradientShift => self.gradient_color(char_index, text_width),
597 AnimatedTextEffect::Sparkle => self.sparkle_color(char_index),
598 };
599
600 let style = base_style.fg(fg_color);
601
602 if x as usize + ch_width <= (area.x + area.width) as usize {
604 buf.set_string(x, y, ch.to_string(), style);
605 x += ch_width as u16;
606 }
607 }
608
609 while x < area.x + area.width {
611 buf.set_string(x, y, " ", base_style);
612 x += 1;
613 }
614 }
615}
616
617#[cfg(test)]
618mod tests {
619 use super::*;
620
621 #[test]
622 fn test_animated_text_state_new() {
623 let state = AnimatedTextState::new();
624 assert_eq!(state.frame, 0);
625 assert_eq!(state.wave_position, 0);
626 assert!(state.active);
627 }
628
629 #[test]
630 fn test_animated_text_state_with_interval() {
631 let state = AnimatedTextState::with_interval(100);
632 assert_eq!(state.interval, Duration::from_millis(100));
633 }
634
635 #[test]
636 fn test_animated_text_state_reset() {
637 let mut state = AnimatedTextState::new();
638 state.frame = 128;
639 state.wave_position = 10;
640 state.wave_direction = WaveDirection::Backward;
641
642 state.reset();
643
644 assert_eq!(state.frame, 0);
645 assert_eq!(state.wave_position, 0);
646 assert_eq!(state.wave_direction, WaveDirection::Forward);
647 }
648
649 #[test]
650 fn test_animated_text_state_start_stop() {
651 let mut state = AnimatedTextState::new();
652 assert!(state.is_active());
653
654 state.stop();
655 assert!(!state.is_active());
656
657 state.start();
658 assert!(state.is_active());
659 }
660
661 #[test]
662 fn test_animated_text_state_interpolation() {
663 let mut state = AnimatedTextState::new();
664
665 let factor = state.interpolation_factor();
667 assert!((factor - 0.5).abs() < 0.1);
668
669 state.frame = 64;
671 let factor = state.interpolation_factor();
672 assert!(factor > 0.8);
673
674 state.frame = 192;
676 let factor = state.interpolation_factor();
677 assert!(factor < 0.2);
678 }
679
680 #[test]
681 fn test_animated_text_style_presets() {
682 let pulse = AnimatedTextStyle::pulse(Color::Red, Color::Blue);
683 assert_eq!(pulse.effect, AnimatedTextEffect::Pulse);
684 assert_eq!(pulse.primary_color, Color::Red);
685 assert_eq!(pulse.secondary_color, Color::Blue);
686
687 let wave = AnimatedTextStyle::wave(Color::White, Color::Yellow);
688 assert_eq!(wave.effect, AnimatedTextEffect::Wave);
689
690 let rainbow = AnimatedTextStyle::rainbow();
691 assert_eq!(rainbow.effect, AnimatedTextEffect::Rainbow);
692 }
693
694 #[test]
695 fn test_animated_text_style_builder() {
696 let style = AnimatedTextStyle::new()
697 .effect(AnimatedTextEffect::Wave)
698 .primary_color(Color::Green)
699 .secondary_color(Color::Cyan)
700 .wave_width(5)
701 .bold();
702
703 assert_eq!(style.effect, AnimatedTextEffect::Wave);
704 assert_eq!(style.primary_color, Color::Green);
705 assert_eq!(style.secondary_color, Color::Cyan);
706 assert_eq!(style.wave_width, 5);
707 assert!(style.modifiers.contains(Modifier::BOLD));
708 }
709
710 #[test]
711 fn test_animated_text_style_presets_themed() {
712 let success = AnimatedTextStyle::success();
713 assert_eq!(success.primary_color, Color::Green);
714
715 let warning = AnimatedTextStyle::warning();
716 assert_eq!(warning.primary_color, Color::Yellow);
717
718 let error = AnimatedTextStyle::error();
719 assert_eq!(error.primary_color, Color::Red);
720
721 let info = AnimatedTextStyle::info();
722 assert_eq!(info.effect, AnimatedTextEffect::Wave);
723 }
724
725 #[test]
726 fn test_animated_text_display_width() {
727 let state = AnimatedTextState::new();
728 let text = AnimatedText::new("Hello", &state);
729 assert_eq!(text.display_width(), 5);
730
731 let text = AnimatedText::new("Hello World", &state);
732 assert_eq!(text.display_width(), 11);
733 }
734
735 #[test]
736 fn test_animated_text_render() {
737 let state = AnimatedTextState::new();
738 let text = AnimatedText::new("Test", &state);
739
740 let mut buf = Buffer::empty(Rect::new(0, 0, 10, 1));
741 text.render(Rect::new(0, 0, 10, 1), &mut buf);
742 }
744
745 #[test]
746 fn test_animated_text_render_wave() {
747 let mut state = AnimatedTextState::new();
748 state.wave_position = 2;
749
750 let style = AnimatedTextStyle::wave(Color::White, Color::Yellow);
751 let text = AnimatedText::new("Hello", &state).style(style);
752
753 let mut buf = Buffer::empty(Rect::new(0, 0, 10, 1));
754 text.render(Rect::new(0, 0, 10, 1), &mut buf);
755 }
757
758 #[test]
759 fn test_animated_text_render_rainbow() {
760 let state = AnimatedTextState::new();
761 let style = AnimatedTextStyle::rainbow();
762 let text = AnimatedText::new("Rainbow!", &state).style(style);
763
764 let mut buf = Buffer::empty(Rect::new(0, 0, 10, 1));
765 text.render(Rect::new(0, 0, 10, 1), &mut buf);
766 }
768
769 #[test]
770 fn test_animated_text_render_empty_area() {
771 let state = AnimatedTextState::new();
772 let text = AnimatedText::new("Test", &state);
773
774 let mut buf = Buffer::empty(Rect::new(0, 0, 0, 0));
775 text.render(Rect::new(0, 0, 0, 0), &mut buf);
776 }
778
779 #[test]
780 fn test_interpolate_color_rgb() {
781 let c1 = Color::Rgb(0, 0, 0);
783 let c2 = Color::Rgb(255, 255, 255);
784
785 let result = AnimatedText::interpolate_color(c1, c2, 0.5);
786 if let Color::Rgb(r, g, b) = result {
787 assert!((r as i16 - 127).abs() <= 1);
788 assert!((g as i16 - 127).abs() <= 1);
789 assert!((b as i16 - 127).abs() <= 1);
790 } else {
791 panic!("Expected RGB color");
792 }
793 }
794
795 #[test]
796 fn test_interpolate_color_non_rgb() {
797 let c1 = Color::Red;
799 let c2 = Color::Blue;
800
801 assert_eq!(AnimatedText::interpolate_color(c1, c2, 0.3), Color::Red);
802 assert_eq!(AnimatedText::interpolate_color(c1, c2, 0.7), Color::Blue);
803 }
804
805 #[test]
806 fn test_wave_direction_changes() {
807 let mut state = AnimatedTextState::new();
808 state.interval = Duration::from_millis(0); state.last_tick = Some(Instant::now() - Duration::from_secs(1));
810
811 let text_width = 10;
813 for _ in 0..15 {
814 state.tick_with_text_width(text_width);
815 }
816
817 assert_eq!(state.wave_direction, WaveDirection::Backward);
819 }
820}