Skip to main content

rustlens_lib/ui/
splash.rs

1//! Splash screen with waves animation shown before the main TUI.
2
3use crossterm::event::{self, Event, KeyEventKind};
4use ratatui::{
5    backend::CrosstermBackend,
6    layout::{Alignment, Constraint, Direction, Layout, Rect},
7    style::Modifier,
8    text::{Line, Span},
9    widgets::{Block, Borders, Paragraph, Widget},
10    Frame, Terminal,
11};
12use std::io;
13use std::time::{Duration, Instant};
14
15use crate::ui::theme::Theme;
16
17const SPLASH_DURATION: Duration = Duration::from_millis(2200);
18const WAVE_ROWS: usize = 8;
19const WAVE_CHARS: &str = "~∿∼〜〰︴";
20
21/// Draw a single frame of the waves animation. `phase` is in 0.0..1.0 (or more for continuous).
22fn draw_waves(frame: &mut Frame, area: Rect, phase: f64, theme: &Theme) {
23    let wave_len = 24usize;
24    let width = area.width as usize;
25    let height = area.height.saturating_sub(2) as usize;
26    if height == 0 || width == 0 {
27        return;
28    }
29    for row in 0..height.min(WAVE_ROWS) {
30        let y = area.y + 2 + row as u16;
31        let mut line = String::with_capacity(width);
32        let row_phase = phase + row as f64 * 0.2;
33        for col in 0..width {
34            let t = (col as f64 / wave_len as f64) + row_phase;
35            let wave = (t * std::f64::consts::TAU).sin();
36            let idx = ((wave + 1.0) * 0.5 * (WAVE_CHARS.len() - 1) as f64) as usize;
37            let idx = idx.min(WAVE_CHARS.len() - 1);
38            let c = WAVE_CHARS.chars().nth(idx).unwrap_or('~');
39            line.push(c);
40        }
41        let style = if row % 2 == 0 {
42            theme.style_accent()
43        } else {
44            theme.style_dim()
45        };
46        let span = Span::styled(line, style);
47        frame.render_widget(Paragraph::new(span), Rect::new(area.x, y, area.width, 1));
48    }
49}
50
51/// Run the splash screen: waves animation + title. Returns when duration elapsed or any key pressed.
52pub fn run_splash(terminal: &mut Terminal<CrosstermBackend<io::Stdout>>) -> anyhow::Result<()> {
53    let theme = Theme::default();
54    let start = Instant::now();
55
56    loop {
57        let elapsed = start.elapsed();
58        if elapsed >= SPLASH_DURATION {
59            break;
60        }
61        let phase = elapsed.as_secs_f64() * 2.0;
62
63        terminal.draw(|frame| {
64            let area = frame.area();
65            let chunks = Layout::default()
66                .direction(Direction::Vertical)
67                .constraints([
68                    Constraint::Length(3),
69                    Constraint::Min(10),
70                    Constraint::Length(3),
71                ])
72                .split(area);
73
74            let title = Paragraph::new(vec![
75                Line::from(""),
76                Line::from(vec![
77                    Span::styled(
78                        "RUSTLENS",
79                        theme.style_accent_bold().add_modifier(Modifier::BOLD),
80                    ),
81                    Span::styled("  ·  ", theme.style_muted()),
82                    Span::styled("Rust Code Inspector", theme.style_dim()),
83                ]),
84            ])
85            .alignment(Alignment::Center)
86            .block(
87                Block::default()
88                    .borders(Borders::BOTTOM)
89                    .border_style(theme.style_border()),
90            );
91            title.render(chunks[0], frame.buffer_mut());
92
93            draw_waves(frame, chunks[1], phase, &theme);
94
95            let hint = Paragraph::new(Line::from(vec![
96                Span::styled("Starting... ", theme.style_muted()),
97                Span::styled("(press any key to skip)", theme.style_dim()),
98            ]))
99            .alignment(Alignment::Center);
100            hint.render(chunks[2], frame.buffer_mut());
101        })?;
102
103        if event::poll(Duration::from_millis(50))? {
104            if let Event::Key(key) = event::read()? {
105                if key.kind == KeyEventKind::Press {
106                    break;
107                }
108            }
109        }
110    }
111
112    Ok(())
113}