Skip to main content

textyle/animation/
mod.rs

1use anyhow::Result;
2use crossterm::event::KeyEvent;
3
4use crate::{canvas::TextCanvas, layout::{geometry::Size, Layout}};
5use defer_lite::defer;
6
7pub trait AnimationState: Clone {}
8impl <T: Clone> AnimationState for T {}
9
10pub type KeyCode = crossterm::event::KeyCode;
11pub type KeyModifiers = crossterm::event::KeyModifiers;
12
13#[derive(Clone)]
14pub enum AnimationEvent {
15    KeyEvent(KeyCode, KeyModifiers),
16    Resize(usize, usize)
17}
18
19#[derive(Clone)]
20pub enum AnimationCommand {
21    Quit
22}
23
24#[derive(Clone)]
25pub struct AnimationContext<State: AnimationState> {
26    pub frame_count: usize,
27    pub delta_milis: f64,
28    pub state: State,
29    pub pending_events: Vec<AnimationEvent>,
30    pub commands: Vec<AnimationCommand>
31}
32
33pub type PlainAnimationContext = AnimationContext<()>;
34impl Default for PlainAnimationContext {
35    fn default() -> Self {
36        AnimationContext {
37            frame_count: 0,
38            delta_milis: 0.0,
39            state: (),
40            pending_events: vec![],
41            commands: vec![]
42        }
43    }
44}
45
46impl<T: Clone> AnimationContext<T> {
47    pub fn add_command(&mut self, command: AnimationCommand) {
48        self.commands.push(command)
49    }
50}
51
52#[derive(PartialEq)]
53pub enum AnimationBuffer {
54    Main,
55    Alternate
56}
57
58impl Default for AnimationBuffer {
59    fn default() -> Self {
60        Self::Main
61    }
62}
63
64#[derive(Default)]
65pub struct AnimationRunConfig {
66    pub buffer_type: AnimationBuffer
67}
68
69type AnimatedLayoutProvider<State> = fn(&AnimationContext<State>)->Layout<AnimationContext<State>>;
70pub struct AnimatedTextCanvas<State: AnimationState> {
71    layout: AnimatedLayoutProvider<State>,
72    update: fn(&mut AnimationContext<State>)
73}
74
75impl<State: AnimationState> AnimatedTextCanvas<State> {
76    fn clear_buffer(&self) {
77        crossterm::execute!(
78            std::io::stdout(),
79            crossterm::terminal::Clear(crossterm::terminal::ClearType::Purge),
80            crossterm::cursor::MoveTo(0, 0),
81        ).unwrap();
82    }
83    
84    pub fn set_update(&mut self, update_fn: fn(&mut AnimationContext<State>)) {
85        self.update = update_fn;
86    }
87}
88
89impl<State: AnimationState> AnimatedTextCanvas<State> {
90    pub fn new(layout: AnimatedLayoutProvider<State>) -> Self {
91        AnimatedTextCanvas { layout, update: |_|{} }
92    }
93
94    pub fn run_with_state(&self, state: State, config: AnimationRunConfig) -> Result<()> {
95        let mut stdout = std::io::stdout();
96
97        let (terminal_columns, terminal_rows ) = crossterm::terminal::size().unwrap();
98
99        let mut terminal_columns = terminal_columns as usize;
100        let mut terminal_rows = terminal_rows as usize;
101
102        let bounds = &Size::new(terminal_columns, terminal_rows);
103        // let bounds = &Rect::sized(20, 5);
104        let mut canvas = TextCanvas::create_in_bounds(bounds);
105
106        let mut context = AnimationContext {
107            frame_count: 0,
108            delta_milis: 0.0,
109            state,
110            pending_events: vec![],
111            commands: vec![]
112        };
113
114        let layout = (self.layout)(&mut context);
115
116        canvas.render_layout(&layout, &mut context);
117
118        crossterm::terminal::enable_raw_mode().unwrap_or_else(|_| {
119            crossterm::terminal::disable_raw_mode().unwrap();
120        });
121        defer! { let _ = crossterm::terminal::disable_raw_mode(); }
122
123        let mut last_time = std::time::Instant::now();
124
125        if config.buffer_type == AnimationBuffer::Alternate {
126            crossterm::execute!(stdout, crossterm::terminal::EnterAlternateScreen)?;
127        }
128
129        defer!{
130            if config.buffer_type == AnimationBuffer::Alternate {
131                crossterm::execute!(std::io::stdout(), crossterm::terminal::LeaveAlternateScreen)
132                .unwrap_or_else(|err| { println!("Error exiting alternate screen buffer:\n{err}"); });
133            }
134        }
135
136        crossterm::execute!(stdout, crossterm::cursor::Hide)?;
137        
138        defer!{
139            crossterm::execute!(std::io::stdout(), crossterm::cursor::Show)
140                .unwrap_or_else(|err| { println!("Error restoring cursor state:\n{err}"); });
141        }
142
143        loop {
144            (self.update)(&mut context);
145
146            let mut should_stop = false;
147
148            for command in &context.commands {
149                match command {
150                    AnimationCommand::Quit => {
151                        should_stop = true;
152                    }
153                }
154            }
155
156            if should_stop { break; }
157
158            context.delta_milis = last_time.elapsed().as_secs_f64().clamp(0.000001, f64::MAX) * 1000.0;
159            last_time = std::time::Instant::now();
160            canvas.draw_on_buffer();
161            
162            if crossterm::event::poll(std::time::Duration::from_millis(1))? {
163                match crossterm::event::read() {
164                    Ok(event) => {
165                        if let crossterm::event::Event::Key(KeyEvent { code: crossterm::event::KeyCode::Esc, .. }) = event {
166                            break;
167                        } else if let crossterm::event::Event::Key(KeyEvent { code: crossterm::event::KeyCode::Char('c'), modifiers, .. }) = event {
168                            if modifiers.contains(crossterm::event::KeyModifiers::CONTROL) {
169                                break;
170                            }
171                        } else if let crossterm::event::Event::Resize(columns, rows) = event {
172                            terminal_columns = columns as usize;
173                            terminal_rows = rows as usize;
174        
175                            let bounds = &Size::new(terminal_columns, terminal_rows);
176                            canvas = TextCanvas::create_in_bounds(bounds);
177                            context.pending_events.push(AnimationEvent::Resize(terminal_columns, terminal_rows));
178                        } else if let crossterm::event::Event::Key(e) = event {
179                            context.pending_events.push(AnimationEvent::KeyEvent(e.code, e.modifiers));
180                        }
181                    }
182                    Err(err) => {
183                        crossterm::execute!(stdout, crossterm::terminal::LeaveAlternateScreen, crossterm::style::Print(format!("{err}")), crossterm::terminal::EnterAlternateScreen)?;
184                        break;
185                    }
186                };
187            }
188
189            canvas.clear_with(" ");
190
191            let layout = (self.layout)(&mut context);
192            canvas.render_layout(&layout, &mut context);
193            
194            self.clear_buffer();
195            context.frame_count += 1;
196        }
197
198        Ok(())
199    }
200}
201
202impl AnimatedTextCanvas<()> {
203    pub fn run(&self, config: AnimationRunConfig) -> Result<()> {
204        self.run_with_state((), config)
205    }
206}