aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Cargo.lock32
-rw-r--r--Cargo.toml1
-rw-r--r--src/generation.rs177
-rw-r--r--src/main.rs6
-rw-r--r--src/ui.rs219
5 files changed, 370 insertions, 65 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 3c28701..6b8dcdd 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -26,6 +26,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
+name = "cassowary"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53"
+
+[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -85,6 +91,7 @@ dependencies = [
"colored",
"crossterm",
"rand",
+ "tui",
]
[[package]]
@@ -250,6 +257,31 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2fd0db749597d91ff862fd1d55ea87f7855a744a8425a64695b6fca237d1dad1"
[[package]]
+name = "tui"
+version = "0.19.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ccdd26cbd674007e649a272da4475fb666d3aa0ad0531da7136db6fab0e5bad1"
+dependencies = [
+ "bitflags",
+ "cassowary",
+ "crossterm",
+ "unicode-segmentation",
+ "unicode-width",
+]
+
+[[package]]
+name = "unicode-segmentation"
+version = "1.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7e8820f5d777f6224dc4be3632222971ac30164d4a258d595640799554ebfd99"
+
+[[package]]
+name = "unicode-width"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973"
+
+[[package]]
name = "wasi"
version = "0.11.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
diff --git a/Cargo.toml b/Cargo.toml
index 3e36dcb..fb0554f 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -9,3 +9,4 @@ edition = "2021"
colored = "2.0.0"
crossterm = "0.25.0"
rand = "0.8.5"
+tui = "0.19.0"
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<Vec<Cell>>;
@@ -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<Cell>> = Vec::new();
for _ in 0..cols {
let mut col: Vec<Cell> = Vec::new();
@@ -56,6 +64,21 @@ pub fn new_gen() -> Gen {
colums
}
+pub fn gen_to_spans(gen: &Gen) -> Vec<Spans> {
+ 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<Cell> = 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<dyn Error>> {
+ // 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<dyn Error>> {
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<T> {
+ state: ListState,
+ items: Vec<T>,
+}
+
+impl<T> StatefulList<T> {
+ fn with_items(items: Vec<T>) -> StatefulList<T> {
+ 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<B: Backend>(
+ terminal: &mut Terminal<B>,
+ 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<B: Backend>(f: &mut Frame<B>, 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<ListItem> = 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]);
+}