//! Fade through RGB colors showing off some layouting //! and event info use log::info; use std::io; use std::marker::PhantomData; use termit::prelude::*; #[async_std::main] async fn main() -> io::Result<()> { env_logger::init(); let mut app = AppState::default(); let mut termit = Terminal::try_system_default()? //.controlling(fs::File::open("/dev/tty")?) .into_termit::() .enter_raw_mode()? .capture_mouse(true)? .handle_focus_events(true)? .use_alternate_screen(true)? .show_cursor(false)? .true_color(true); let mut ui = Stack::layered_boxes(vec![]) .add_box(Canvas::new(Ball::default()).back(Color::black())) .add_box(Pretty::new(Fps::default()).front(Color::black())) .add_box(Exit); termit .refresh_time_sender() .send_blocking(Duration::from_millis(15)) .expect("refresh interval update"); while !app.is_terminating { termit.step(&mut app, &mut ui).await?; } Ok(()) } #[derive(Default)] struct AppState { pub is_terminating: bool, } struct Exit; impl Widget for Exit { fn update( &mut self, model: &mut AppState, input: &Event, _screen: &mut Screen, painter: &Painter, ) -> Window { if matches!( input, Event::Key(KeyEvent { keycode: KeyCode::Char('q'), .. }) ) { info!("exit!"); model.is_terminating = true; } painter.scope().trim(point(0, 0)) } } #[derive(Default)] struct Ball { pos: Point, direction: bool, phantom_model: PhantomData, phantom_appe_event: PhantomData, } impl Widget for Ball { fn update( &mut self, _model: &mut M, input: &Event, screen: &mut Screen, painter: &Painter, ) -> Window { if let Event::Refresh(beats) = input { // mumbo jumbo rendering for row in painter.scope().row..painter.scope().height { for col in painter.scope().col..painter.scope().width { let dx = col as f32 - self.pos.col as f32; let dy = 2.0 * row as f32 - 2.0 * self.pos.row as f32; let d = (dx * dx + dy * dy).sqrt(); screen.set( point(col, row), &Style::DEFAULT.back(Color::rgb( (255.0 * 10.0 / d).clamp(1.0, 254.0) as u8, 255 - (255.0 * self.pos.row as f32 / painter.scope().height as f32) .clamp(1.0, 254.0) as u8, (d * 5.0).clamp(1.0, 254.0) as u8, )), " ", ); } } for _ in 0..*beats { // movement if self.pos.col >= painter.scope().width + painter.scope().row || self.pos.col <= 0 { self.direction = !self.direction } self.pos.row = (painter.scope().height as f32 * (self.pos.col as f32 / 10.0).sin() / 4.0 + painter.scope().row as f32 + painter.scope().height as f32 / 2.0) as u16; self.pos.col = (self.pos.col as i32) .saturating_add(2 * (self.direction) as i32 - 1) .clamp(0, screen.size().col as i32) as u16; } } painter.scope() } } use std::{ collections::VecDeque, time::{Duration, Instant}, }; /// Reduce cycling if not in use pub struct Fps { durations: VecDeque, last: Instant, show: bool, } impl Default for Fps { fn default() -> Self { Self { durations: vec![Duration::ZERO; 600].into_iter().collect(), last: Instant::now(), show: true, } } } impl Widget for Fps { fn update( &mut self, model: &mut AppState, input: &Event, screen: &mut Screen, painter: &Painter, ) -> Window { if let (Event::Refresh(_), true) = (input, self.show) { self.durations.push_back(self.last.elapsed()); self.durations.pop_front(); self.last = Instant::now(); // show stats over longer period let (count, total_duration) = self.durations.iter().rev().fold((0, 0.0), |(c, t), a| { if t < 3.0 { (c + 1, t + a.as_secs_f64()) } else { (c, t) } }); let fps = count as f64 / total_duration; format!("FPS:{fps:0.5}").update_asserted(model, input, screen, painter) } else { // no space taken painter.scope().trim(point(0, 0)) } } }