From 3a6b9713f98a3a261554f3187a0c91678128eca9 Mon Sep 17 00:00:00 2001 From: Isaac Mills Date: Mon, 28 Jul 2025 13:57:42 -0600 Subject: [PATCH] Add tab transitions --- thecockpit/src/app/mod.rs | 161 ++++++++++++++++++++++++++++---------- 1 file changed, 120 insertions(+), 41 deletions(-) diff --git a/thecockpit/src/app/mod.rs b/thecockpit/src/app/mod.rs index 7590cac..fffc904 100644 --- a/thecockpit/src/app/mod.rs +++ b/thecockpit/src/app/mod.rs @@ -1,9 +1,14 @@ +use std::{ + rc::Rc, + sync::atomic::{AtomicUsize, Ordering}, +}; + use rand::{rngs::OsRng, seq::IndexedRandom, TryRngCore}; #[cfg(target_arch = "wasm32")] use ratzilla::ratatui; use ratatui::{ - layout::{Constraint, Direction, Layout}, + layout::{Constraint, Direction, Layout, Rect}, style::{Color, Style, Stylize}, widgets::{Block, Borders, Paragraph, Tabs}, Frame, @@ -15,10 +20,51 @@ use crate::app::password_locker::PasswordLocker; mod password_locker; +#[derive(Default, Debug, Clone)] +struct SelectedTab { + currently_selected: Rc, + transitioning_to: Rc, +} + +impl Shader for SelectedTab { + fn name(&self) -> &'static str { + "selected_tab" + } + + fn execute( + &mut self, + _duration: tachyonfx::Duration, + _area: Rect, + _buf: &mut ratatui::prelude::Buffer, + ) { + self.currently_selected.store( + self.transitioning_to.load(Ordering::Relaxed), + Ordering::Relaxed, + ); + } + + fn done(&self) -> bool { + self.currently_selected.load(Ordering::Relaxed) + == self.transitioning_to.load(Ordering::Relaxed) + } + + fn clone_box(&self) -> Box { + Box::new(self.clone()) + } + + fn area(&self) -> Option { + None + } + + fn set_area(&mut self, _area: Rect) {} + + fn filter(&mut self, _filter: tachyonfx::CellFilter) {} +} + pub struct App { tabs: [&'static str; 3], transition_instant: Instant, - selected_tab: usize, + selected_tab: SelectedTab, password_locker: PasswordLocker, current_effect: Effect, } @@ -68,57 +114,71 @@ impl App { Self { tabs: TABS.choose_multiple_array(&mut OsRng.unwrap_err()).unwrap(), transition_instant: Instant::now(), - selected_tab: 0, + selected_tab: SelectedTab::default(), password_locker: PasswordLocker::default(), current_effect: fx::fade_from_fg(Color::Black, (0, Interpolation::CircIn)), } } pub fn draw(&mut self, frame: &mut Frame) { - if self.password_locker.unlocked() { - self.draw_rest(frame); - } - self.draw_passcode(frame); let area = frame.area(); - self.current_effect.process( - self.transition_instant.elapsed().into(), - frame.buffer_mut(), - area, - ); - } + let layout = Layout::new( + Direction::Vertical, + [Constraint::Length(3), Constraint::Min(0)], + ) + .split(area); - pub fn draw_passcode(&mut self, frame: &mut Frame) { - frame.render_widget(&mut self.password_locker, frame.area()); + if self.password_locker.unlocked() { + frame.render_widget( + Tabs::new(self.tabs) + .select(self.selected_tab.transitioning_to.load(Ordering::Relaxed)) + .fg(Color::Rgb(226, 190, 89)) + .bg(Color::Black) + .highlight_style(Style::new().bold()) + .block(Block::new().borders(Borders::all()).title("Coming soon")), + layout[0], + ); + + match self.selected_tab.currently_selected.load(Ordering::Relaxed) { + 0 => self.draw_first_tab(frame, layout[1]), + 1 | 2 => self.draw_second_tab(frame, layout[1]), + _ => {} + } + } + + frame.render_widget(&mut self.password_locker, area); if let Some(cursor_position) = self.password_locker.cursor_position() { if self.password_locker.current_effect_done() { frame.set_cursor_position(cursor_position); } } + + self.current_effect.process( + self.transition_instant.elapsed().into(), + frame.buffer_mut(), + layout[1], + ); } - pub fn draw_rest(&mut self, frame: &mut Frame) { - let layout = Layout::new( - Direction::Vertical, - [Constraint::Length(3), Constraint::Min(0)], - ) - .split(frame.area()); - frame.render_widget( - Tabs::new(self.tabs) - .select(self.selected_tab) - .fg(Color::Rgb(226, 190, 89)) - .bg(Color::Black) - .highlight_style(Style::new().bold()) - .block(Block::new().borders(Borders::all()).title("Coming soon")), - layout[0], - ); + pub fn draw_first_tab(&mut self, frame: &mut Frame, layout: Rect) { frame.render_widget( Paragraph::new(SPLASH) .alignment(ratatui::layout::Alignment::Center) .fg(Color::Rgb(226, 190, 89)) .bg(Color::Black) .block(Block::new().borders(Borders::all())), - layout[1], + layout, + ); + } + + pub fn draw_second_tab(&mut self, frame: &mut Frame, layout: Rect) { + frame.render_widget( + Block::new() + .borders(Borders::all()) + .fg(Color::Rgb(226, 190, 89)) + .bg(Color::Black), + layout, ); } @@ -127,20 +187,39 @@ impl App { } pub fn next_tab(&mut self) { - self.transition_instant = Instant::now(); - if self.selected_tab == self.tabs.len() - 1 { - self.selected_tab = 0; - } else { - self.selected_tab += 1; + if self + .selected_tab + .transitioning_to + .fetch_add(1, Ordering::Relaxed) + == self.tabs.len() - 1 + { + self.selected_tab + .transitioning_to + .store(0, Ordering::Relaxed); } + self.tab_transition(); } pub fn prev_tab(&mut self) { - self.transition_instant = Instant::now(); - if self.selected_tab == 0 { - self.selected_tab = self.tabs.len() - 1; - } else { - self.selected_tab -= 1; + if self + .selected_tab + .transitioning_to + .fetch_sub(1, Ordering::Relaxed) + == 0 + { + self.selected_tab + .transitioning_to + .store(self.tabs.len() - 1, Ordering::Relaxed); } + self.tab_transition(); + } + + fn tab_transition(&mut self) { + self.transition_instant = Instant::now(); + self.current_effect = fx::sequence(&[ + fx::dissolve((5000, Interpolation::Linear)), + Effect::new(self.selected_tab.clone()), + fx::coalesce((10000, Interpolation::Linear)), + ]); } }