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 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}