From 1ce581a931352192c1360e1496d7a1ae56380e9d Mon Sep 17 00:00:00 2001 From: omagdy7 Date: Tue, 13 Sep 2022 16:32:43 +0200 Subject: Added ui.rs which is responsible for the tui and changed the code to run the game of life in a block of the tui --- src/generation.rs | 177 +++++++++++++++++++++++++++---------------- src/main.rs | 6 +- src/ui.rs | 219 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 337 insertions(+), 65 deletions(-) create mode 100644 src/ui.rs (limited to 'src') diff --git a/src/generation.rs b/src/generation.rs index 4188bed..a105d43 100644 --- a/src/generation.rs +++ b/src/generation.rs @@ -1,21 +1,31 @@ #![allow(unused_imports, unused_variables, unused_mut)] +use crate::ui::*; use colored::Colorize; use crossterm::{ cursor::{Hide, MoveTo, Show}, - event, event::{poll, Event, KeyCode, KeyEvent}, + event::{DisableMouseCapture, EnableMouseCapture}, + execute, style::Stylize, terminal, - terminal::{EnterAlternateScreen, LeaveAlternateScreen}, - ExecutableCommand, QueueableCommand, + terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, }; use rand::{thread_rng, Rng}; use std::{ + error::Error, io::{self, Write}, thread::sleep, time::Duration, }; +use tui::{ + backend::{Backend, CrosstermBackend}, + layout::{Alignment, Constraint, Corner, Direction, Layout, Rect}, + style::{Color, Modifier, Style}, + text::{Span, Spans}, + widgets::{Block, Borders, List, ListItem, ListState, Paragraph}, + Frame, Terminal, +}; pub type Gen = Vec>; @@ -25,25 +35,23 @@ pub enum Cell { Dead, } -pub fn render_gen(stdout: &mut io::Stdout, gen: &Gen) { - for i in 0..gen.len() { - for j in 0..gen[0].len() { - stdout.queue(MoveTo(i as u16, j as u16)).unwrap(); +pub fn render_gen(chunk: &Rect, gen: &Gen) { + for i in 0..chunk.height as usize { + for j in 0..chunk.width as usize { match gen[i][j] { // Cell::Alive => print!("😎"), // Cell::Alive => print!("🦀"), Cell::Alive => print!("{}", "X".color("blue")), Cell::Dead => print!("{}", "-".color("red")), } - stdout.flush().unwrap(); } } } -pub fn new_gen() -> Gen { +pub fn new_gen(chunk: &Rect) -> Gen { let cells = vec![Cell::Dead, Cell::Dead, Cell::Alive, Cell::Dead, Cell::Alive]; - let rows: u16 = terminal::size().unwrap().1; - let cols: u16 = terminal::size().unwrap().0; + let cols: u16 = chunk.height; + let rows: u16 = chunk.width; let mut colums: Vec> = Vec::new(); for _ in 0..cols { let mut col: Vec = Vec::new(); @@ -56,6 +64,21 @@ pub fn new_gen() -> Gen { colums } +pub fn gen_to_spans(gen: &Gen) -> Vec { + let mut spans = vec![]; + for i in 0..gen.len() { + let mut txt = String::new(); + for j in 0..gen[0].len() { + match gen[i][j] { + Cell::Alive => txt.push('X'), + Cell::Dead => txt.push('-'), + } + } + spans.push(Spans::from(txt)); + } + spans +} + pub fn is_valid_idx(i: i32, j: i32, m: i32, n: i32) -> bool { i >= 0 && i < m && j >= 0 && j < n } @@ -83,9 +106,10 @@ pub fn get_alive(x: i32, y: i32, cur_gen: &Gen) -> i32 { alive_cnt } -pub fn next_gen(cur_gen: &mut Gen) -> Gen { - let m: i32 = cur_gen.len() as i32; - let n: i32 = cur_gen[0].len() as i32; +pub fn next_gen(app: &mut App) -> Gen { + app.flag = true; + let m: i32 = app.cur_gen.len() as i32; + let n: i32 = app.cur_gen[0].len() as i32; let mut nxt_gen: Gen = Gen::new(); for _ in 0..m { let mut col: Vec = Vec::new(); @@ -97,9 +121,9 @@ pub fn next_gen(cur_gen: &mut Gen) -> Gen { for i in 0..m { for j in 0..n { - let alive = get_alive(i, j, cur_gen); + let alive = get_alive(i, j, &app.cur_gen); - match cur_gen[i as usize][j as usize] { + match app.cur_gen[i as usize][j as usize] { Cell::Alive => { if alive == 2 || alive == 3 { nxt_gen[i as usize][j as usize] = Cell::Alive; @@ -115,57 +139,84 @@ pub fn next_gen(cur_gen: &mut Gen) -> Gen { } } } - *cur_gen = nxt_gen.clone(); + app.cur_gen = nxt_gen.clone(); nxt_gen } -pub fn init() { +pub fn init() -> Result<(), Box> { + // setup terminal + enable_raw_mode()?; let mut stdout = io::stdout(); - let mut frame: Gen = new_gen(); - let mut nxt; - - terminal::enable_raw_mode().unwrap(); - stdout.execute(EnterAlternateScreen).unwrap(); - stdout.execute(Hide).unwrap(); - - 'gameloop: loop { - while event::poll(Duration::default()).unwrap() { - if let Event::Key(key_event) = event::read().unwrap() { - match key_event.code { - KeyCode::Esc | KeyCode::Char('q') => { - break 'gameloop; - } - KeyCode::Char('s') => { - frame = new_gen(); - render_gen(&mut stdout, &frame) - } - - KeyCode::Char('n') => { - nxt = next_gen(&mut frame); - render_gen(&mut stdout, &nxt) - } - KeyCode::Char('a') => 'animate: loop { - nxt = next_gen(&mut frame); - render_gen(&mut stdout, &nxt); - sleep(Duration::from_millis(16)); - if (poll(Duration::from_millis(1))).unwrap() { - if let Event::Key(k) = event::read().unwrap() { - match k.code { - KeyCode::Char('q') => break 'animate, - _ => {} - } - } - } else { - } - }, - _ => {} - } - } - } + execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?; + let backend = CrosstermBackend::new(stdout); + let mut terminal = Terminal::new(backend)?; + + // create app and run it + let tick_rate = Duration::from_millis(250); + let app = App::new(); + let res = run_app(&mut terminal, app, tick_rate); + + // restore terminal + disable_raw_mode()?; + execute!( + terminal.backend_mut(), + LeaveAlternateScreen, + DisableMouseCapture + )?; + terminal.show_cursor()?; + + if let Err(err) = res { + println!("{:?}", err) } - stdout.execute(Show).unwrap(); - stdout.execute(LeaveAlternateScreen).unwrap(); - terminal::disable_raw_mode().unwrap(); - + Ok(()) + + // let mut stdout = io::stdout(); + // let mut frame: Gen = new_gen(); + // let mut nxt; + // + // terminal::enable_raw_mode().unwrap(); + // stdout.execute(EnterAlternateScreen).unwrap(); + // stdout.execute(Hide).unwrap(); + // + // 'gameloop: loop { + // while event::poll(Duration::default()).unwrap() { + // if let Event::Key(key_event) = event::read().unwrap() { + // match key_event.code { + // KeyCode::Esc | KeyCode::Char('q') => { + // break 'gameloop; + // } + // KeyCode::Char('s') => { + // frame = new_gen(); + // render_gen(&mut stdout, &frame) + // } + // + // KeyCode::Char('n') => { + // nxt = next_gen(&mut frame); + // render_gen(&mut stdout, &nxt) + // } + // KeyCode::Char('a') => 'animate: loop { + // nxt = next_gen(&mut frame); + // render_gen(&mut stdout, &nxt); + // sleep(Duration::from_millis(16)); + // if (poll(Duration::from_millis(1))).unwrap() { + // if let Event::Key(k) = event::read().unwrap() { + // match k.code { + // KeyCode::Char('q') => break 'animate, + // _ => {} + // } + // } + // } else { + // } + // }, + // _ => {} + // } + // } + // } + // } + // + // stdout.execute(Show).unwrap(); + // stdout.execute(LeaveAlternateScreen).unwrap(); + // terminal::disable_raw_mode().unwrap(); + // Ok(()) } diff --git a/src/main.rs b/src/main.rs index 056d16a..7b4544b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,9 +1,11 @@ #![allow(unused_imports, unused_variables, unused_mut)] +use std::error::Error; mod generation; use generation::*; +mod ui; +use ui::*; - -fn main() { +fn main() -> Result<(), Box> { generation::init() } diff --git a/src/ui.rs b/src/ui.rs new file mode 100644 index 0000000..08d615f --- /dev/null +++ b/src/ui.rs @@ -0,0 +1,219 @@ +use crossterm::{ + event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode}, + execute, + terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, +}; +use std::{ + error::Error, + io, + thread::sleep, + time::{Duration, Instant}, +}; +use tui::{ + backend::{Backend, CrosstermBackend}, + layout::{Alignment, Constraint, Corner, Direction, Layout, Rect}, + style::{Color, Modifier, Style}, + text::{Span, Spans}, + widgets::{Block, Borders, List, ListItem, ListState, Paragraph}, + Frame, Terminal, +}; + +use crate::generation::*; + +pub struct StatefulList { + state: ListState, + items: Vec, +} + +impl StatefulList { + fn with_items(items: Vec) -> StatefulList { + StatefulList { + state: ListState::default(), + items, + } + } + + fn next(&mut self) { + let i = match self.state.selected() { + Some(i) => { + if i >= self.items.len() - 1 { + 0 + } else { + i + 1 + } + } + None => 0, + }; + self.state.select(Some(i)); + } + + fn previous(&mut self) { + let i = match self.state.selected() { + Some(i) => { + if i == 0 { + self.items.len() - 1 + } else { + i - 1 + } + } + None => 0, + }; + self.state.select(Some(i)); + } + + fn unselect(&mut self) { + self.state.select(None); + } +} + +/// This struct holds the current state of the app. In particular, it has the `items` field which is a wrapper +/// around `ListState`. Keeping track of the items state let us render the associated widget with its state +/// and have access to features such as natural scrolling. +/// +/// Check the event handling at the bottom to see how to change the state on incoming events. +/// Check the drawing logic for items on how to specify the highlighting style for selected items. +pub struct App<'a> { + pub items: StatefulList<(&'a str, usize)>, + pub flag: bool, + pub layout: Layout, + pub cur_gen: Gen, + pub nxt_gen: Gen, + // generation: Gen, +} + +impl<'a> App<'a> { + pub fn new() -> App<'a> { + App { + items: StatefulList::with_items(vec![ + ("Glider", 1), + ("Glider", 2), + ("Glider", 1), + ("Glider", 3), + ]), + flag: false, + layout: Layout::default() + .direction(Direction::Horizontal) + .constraints([Constraint::Percentage(15), Constraint::Percentage(85)].as_ref()), + cur_gen: Gen::new(), + nxt_gen: Gen::new(), + } + } + + // Rotate through the event list. + // This only exists to simulate some kind of "progress" + // fn on_tick(&mut self) { + // let event = self.events.remove(0); + // self.events.push(event); + // } +} + +pub fn run_app( + terminal: &mut Terminal, + mut app: App, + tick_rate: Duration, +) -> io::Result<()> { + let mut last_tick = Instant::now(); + loop { + terminal.draw(|f| ui(f, &mut app))?; + // let mut frame = app.generation; + // let nxt; + + // let timeout = tick_rate + // .checked_sub(last_tick.elapsed()) + // .unwrap_or_else(|| Duration::from_secs(0)); + let timeout = Duration::from_millis(40); + if crossterm::event::poll(timeout)? { + if let Event::Key(key) = event::read()? { + match key.code { + KeyCode::Char('q') => return Ok(()), + KeyCode::Left | KeyCode::Char('h') => app.items.unselect(), + KeyCode::Down | KeyCode::Char('j') => app.items.next(), + KeyCode::Up | KeyCode::Char('k') => app.items.previous(), + _ => {} // KeyCode::Char('s') => { + // frame = new_gen(); + // render_gen(&mut stdout, &frame); + // Ok(()); + // } + // KeyCode::Char('n') => { + // nxt = next_gen(&mut frame); + // render_gen(&mut stdout, &nxt) + // } + // KeyCode::Char('a') => 'animate: loop { + // nxt = next_gen(&mut frame); + // render_gen(&mut stdout, &nxt); + // sleep(Duration::from_millis(16)); + // if (crossterm::event::poll(Duration::from_millis(1))).unwrap() { + // if let Event::Key(k) = event::read().unwrap() { + // match k.code { + // KeyCode::Char('q') => break 'animate, + // _ => {} + // } + // } + // } else { + // } + // }, + // _ => {} + } + } + } + // if last_tick.elapsed() >= tick_rate { + // app.on_tick(); + // last_tick = Instant::now(); + // } + } +} + +fn ui(f: &mut Frame, app: &mut App) { + // Create two chunks with equal horizontal screen space + // let chunks = Layout::default() + // .direction(Direction::Horizontal) + // .constraints([Constraint::Percentage(15), Constraint::Percentage(85)].as_ref()); + // .split(f.size()); + + let chunks = app.layout.split(f.size()); + // Iterate through all elements in the `items` app and append some debug text to it. + let items: Vec = app + .items + .items + .iter() + .map(|i| { + let lines = vec![Spans::from(i.0)]; + ListItem::new(lines).style(Style::default().fg(Color::Red).bg(Color::Black)) + }) + .collect(); + + // Create a List from all list items and highlight the currently selected one + let items = List::new(items) + .block(Block::default().borders(Borders::ALL).title("List")) + .highlight_style( + Style::default() + .bg(Color::Blue) + .add_modifier(Modifier::BOLD), + ) + .highlight_symbol("> "); + + // We can now render the item list + f.render_stateful_widget(items, chunks[0], &mut app.items.state); + + if !app.flag { + app.cur_gen = new_gen(&chunks[1]); + } + let nxt = next_gen(app); + let spans = gen_to_spans(&nxt); + + let create_block = |title| { + Block::default() + .borders(Borders::ALL) + .style(Style::default().bg(Color::Black).fg(Color::Red)) + .title(Span::styled( + title, + Style::default().add_modifier(Modifier::BOLD), + )) + .title_alignment(Alignment::Center) + }; + let paragraph = Paragraph::new(spans.clone()) + .style(Style::default().bg(Color::Black).fg(Color::Blue)) + .block(create_block(" Game Of Life ")) + .alignment(Alignment::Left); + f.render_widget(paragraph, chunks[1]); +} -- cgit v1.2.3