From b2f2e0fad21670fb01d6bf068bae2db69acca81b Mon Sep 17 00:00:00 2001 From: markichnich Date: Thu, 24 Aug 2023 15:21:32 +0200 Subject: [PATCH] reworking the reworked ui once again, added color --- README.md | 8 +- src/main.rs | 15 +- src/state.rs | 61 +++---- src/sudoku/generator.rs | 8 +- src/ui.rs | 342 ++++++++++++++++++++++------------------ 5 files changed, 236 insertions(+), 198 deletions(-) diff --git a/README.md b/README.md index 1d6ed16..a368b22 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,6 @@ The current control scheme adheres to vim-like keybindings and is modal. - [x] Validate Sudokus - [x] Generate Sudokus - [x] Difficulties to choose - - [ ] Undo functionality - [x] Basic UI @@ -70,12 +69,13 @@ The current control scheme adheres to vim-like keybindings and is modal. - [x] Preselect numbers - [x] Edit Mode to (re)place numbers - [x] Markup Mode to mark where numbers could go + - [ ] Autoremove marks after placing number in edit mode - [x] Go Mode to move to blocks 1-9 - [x] Toggle Number/Mark with Space - [ ] Undo/Redo stack - - [ ] Colored UI - - [ ] Hightlight preselected numbers - - [ ] Hightlight preselected markups + - [x] Colored UI + - [x] Hightlight preselected numbers + - [x] Hightlight preselected markups - [x] Scoreboard - [x] Live timer - [x] Mode indicator diff --git a/src/main.rs b/src/main.rs index 26d49d2..7059835 100644 --- a/src/main.rs +++ b/src/main.rs @@ -16,7 +16,8 @@ use std::{io, time::Duration}; fn main() { let mut screen = Screen::init(io::stdout()); - let mut state = State::init(Difficulty::Mid); + let mut state = State::init(Difficulty::Expert); + screen.draw_board().or_crash(); loop { if poll(Duration::from_millis(250)).unwrap_or(false) { @@ -49,7 +50,7 @@ fn main() { state.toggle_current_cell(); state.enter_next_mode(); if is_solution(&state.board) { - screen.deinit(); + screen.deinit().or_crash(); println!("+------------+"); println!("| You Win :) |"); println!("+------------+"); @@ -81,7 +82,7 @@ fn main() { }, Char('q') | Char('Q') => { - screen.deinit(); + screen.deinit().or_crash(); break; } @@ -91,13 +92,7 @@ fn main() { } } - screen.update_dimensions() - .unwrap_or_else(|_| std::process::exit(1)); - - screen.render(&state); - - screen.draw(state.cur_row, state.cur_col) - .unwrap_or_else(|_| std::process::exit(1)); + screen.draw(&state).or_crash(); } } diff --git a/src/state.rs b/src/state.rs index 5970ad1..ad71ad8 100644 --- a/src/state.rs +++ b/src/state.rs @@ -149,14 +149,14 @@ impl State { } pub fn toggle_current_mark(&mut self) { - self.markups[self.cur_row][self.cur_col][self.preselection as usize] ^= true; + self.markups[self.cur_row][self.cur_col][self.preselection as usize - 1] ^= true; } pub fn delete_current_mark(&mut self) { - self.markups[self.cur_row][self.cur_col][self.preselection as usize] = false; + self.markups[self.cur_row][self.cur_col][self.preselection as usize - 1] = false; } - pub fn get_completion_chars(&self) -> [char; 2] { + pub fn get_completion_string(&self) -> String { let mut count = 0; for row in self.board { for cell in row { @@ -166,7 +166,7 @@ impl State { } } let to_char = |x| (x + b'0') as char; - [to_char(count / 10), to_char(count % 10)] + [to_char(count / 10), to_char(count % 10)].iter().collect() } pub fn get_preselection_completion_char(&self) -> char { @@ -179,37 +179,42 @@ impl State { (count.min(9) as u8 + b'0') as char } - pub fn get_difficulty_chars(&self) -> [char; 5] { + pub fn get_difficulty_string(&self) -> String { match self.difficulty { - Difficulty::Easy => [' ', 'E', 'a', 's', 'y'], - Difficulty::Mid => [' ', 'M', 'i', 'd', ' '], - Difficulty::Hard => [' ', 'H', 'a', 'r', 'd'], - Difficulty::Expert => ['E', 'x', 'p', 'r', 't'], - Difficulty::Custom(x) => [ - 'C', - '(', - (x as u8 / 10 + b'0') as char, - (x as u8 % 10 + b'0') as char, - ')', - ], + Difficulty::Easy => String::from("Easy "), + Difficulty::Mid => String::from(" Mid "), + Difficulty::Hard => String::from("Hard "), + Difficulty::Expert => String::from("Exprt"), + Difficulty::Custom(x) => format!("C({:02})", x), } } - pub fn get_timer_chars(&self) -> [char; 5] { - let mut secs = self.get_time().as_secs(); - let mut chars = [':'; 5]; + pub fn get_timer_strings(&self) -> (String, String) { + let mut n = self.get_time().as_secs(); + + let mut mins = String::with_capacity(2); + let mut secs = String::with_capacity(2); + let to_char = |x: u64| (x as u8 + b'0') as char; - chars[0] = to_char(secs / 600); - secs %= 600; - chars[1] = to_char(secs / 60); - secs %= 60; - chars[3] = to_char(secs / 10); - secs %= 10; - chars[4] = to_char(secs); - chars + + mins.push(to_char(n / 600)); + n %= 600; + mins.push(to_char(n / 60)); + n %= 60; + + secs.push(to_char(n / 10)); + n %= 10; + secs.push(to_char(n)); + + (mins, secs) } pub fn get_timer_string(&self) -> String { - self.get_timer_chars().iter().collect() + let (mins, secs) = self.get_timer_strings(); + let mut timer = String::with_capacity(5); + timer.push_str(&mins); + timer.push(':'); + timer.push_str(&secs); + timer } } diff --git a/src/sudoku/generator.rs b/src/sudoku/generator.rs index 4f15576..219d37b 100644 --- a/src/sudoku/generator.rs +++ b/src/sudoku/generator.rs @@ -25,10 +25,10 @@ impl Difficulty { /// sudoku board of the given `Difficulty` pub fn removal_count(&self) -> usize { match self { - Easy => 35, - Mid => 45, - Hard => 52, - Expert => 62, + Easy => 31, + Mid => 39, + Hard => 49, + Expert => 61, Custom(x) => *x, } } diff --git a/src/ui.rs b/src/ui.rs index 3d46413..436edf2 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -1,9 +1,12 @@ use crate::state::*; + +use std::cmp::Ordering::*; use std::io; use crossterm::{ - cursor::{MoveTo, MoveToColumn, SetCursorStyle}, + cursor::{MoveDown, MoveLeft, MoveRight, MoveTo, MoveToColumn, MoveUp, SetCursorStyle}, execute, queue, + style::{Color, SetBackgroundColor, SetForegroundColor}, terminal::{ disable_raw_mode, enable_raw_mode, size, Clear, ClearType::All, EnterAlternateScreen, LeaveAlternateScreen, @@ -14,7 +17,8 @@ pub struct Screen where T: io::Write, { - pub data: Vec>, + pub presel_color_pair: (Color, Color), + pub markup_color_background: Color, pub ostream: T, pub width: usize, pub height: usize, @@ -28,38 +32,48 @@ where let (width, height) = size().unwrap(); let (width, height) = (width as usize, height as usize); - let data = RENDER_TEMPLATE.iter().map(|s| s.to_vec()).collect(); + let presel_color_pair = (Color::Black, Color::Cyan); + let markup_color_background = Color::Cyan; let mut screen = Screen { - data, + presel_color_pair, + markup_color_background, ostream, width, height, }; - enable_raw_mode().expect("[!]: Error: ui::init: Failed to enable raw mode."); - queue!(screen.ostream, EnterAlternateScreen) - .expect("[!]: Error: ui::init: Failed to enter alternate screen."); + enable_raw_mode().expect("[-]: Error: ui::init: Failed to enable raw mode."); + queue!(screen.ostream, Clear(All), EnterAlternateScreen) + .expect("[-]: Error: ui::init: Failed to enter alternate screen."); screen } - pub fn deinit(&mut self) { - disable_raw_mode().unwrap_or(()); - execute!(self.ostream, LeaveAlternateScreen).unwrap_or(()); + pub fn deinit(&mut self) -> io::Result<()> { + disable_raw_mode()?; + execute!(self.ostream, Clear(All), LeaveAlternateScreen)?; + Ok(()) } pub fn update_dimensions(&mut self) -> io::Result<()> { + let old_dimensions = (self.width, self.height); + let (width, height) = size()?; self.width = width as usize; self.height = height as usize; if height < 14 || width < 54 { self.clear()?; - self.deinit(); + self.deinit()?; eprintln!("[!]: Error: ui::update_dimensions: Terminal size too small to display UI."); return Err(io::Error::from(io::ErrorKind::Other)); } + if old_dimensions != (self.width, self.height) { + self.clear()?; + self.draw_board()?; + } + Ok(()) } @@ -68,101 +82,143 @@ where queue!(self.ostream, MoveToColumn(0)) } - pub fn draw(&mut self, row: usize, col: usize) -> io::Result<()> { - self.draw_screen()?; - self.draw_cursor(row, col)?; - self.ostream.flush()?; + pub fn draw(&mut self, state: &State) -> io::Result<()> { + self.update_dimensions()?; + + self.draw_numbers(state)?; + self.draw_scoreboard(state)?; + self.draw_cursor(state)?; + + self.ostream.flush() + } + + pub fn draw_board(&mut self) -> io::Result<()> { + self.init_cursor_offset()?; + queue!(self.ostream, SetForegroundColor(Color::Reset))?; + queue!(self.ostream, SetBackgroundColor(Color::Reset))?; + + let board_template = board_template(); + + for line in board_template { + write!(self.ostream, "{}", line)?; + self.move_cursor_by(-41, 1)?; + } Ok(()) } - pub fn draw_screen(&mut self) -> io::Result<()> { - self.clear()?; - let lft_pad = self.width / 2 - 14; - let bot_pad = self.height / 2 - 7; + fn draw_numbers(&mut self, state: &State) -> io::Result<()> { + self.init_cursor_offset()?; + queue!(self.ostream, SetForegroundColor(Color::Reset))?; + queue!(self.ostream, SetBackgroundColor(Color::Reset))?; + self.move_cursor_by(1, 0)?; - let mut display = String::with_capacity((43 + lft_pad) * 13 + bot_pad * 2); - - display.extend(self.data.iter().flat_map(|line| { - std::iter::repeat(' ') - .take(lft_pad) - .chain(line.to_owned()) - .chain(['\r', '\n']) - })); - - display.push_str(&"\r\n".repeat(bot_pad)); - - write!(self.ostream, "{}", display)?; - Ok(()) - } - - pub fn render(&mut self, state: &State) { - self.render_board(); - self.render_board_cells(state); - self.render_scoreboard_elements(state); - } - - fn render_board(&mut self) { - self.data = RENDER_TEMPLATE.iter().map(|s| s.to_vec()).collect(); - } - - fn render_board_cells(&mut self, state: &State) { for row in 0..9 { + match row { + 3 | 6 => self.move_cursor_by(0, 2), + _ => self.move_cursor_by(0, 1), + }?; + for col in 0..9 { - let i = row + 1 + row / 3; - let j = col + 2 + col + col / 3 * 3; + match col { + 3 | 6 => self.move_cursor_by(4, 0), + _ => self.move_cursor_by(1, 0), + }?; - self.data[i][j] = match state.board[row][col] { - 0 => ' ', - x => (x + b'0') as char, - } + let chr = match state.board[row][col] { + x if x == state.preselection => { + queue!( + self.ostream, + SetForegroundColor(self.presel_color_pair.0), + SetBackgroundColor(self.presel_color_pair.1) + ) + .unwrap_or(()); + (x + b'0') as char + } + 0 => { + match state.markups[row][col][state.preselection as usize - 1] { + true => { + queue!( + self.ostream, + SetBackgroundColor(self.markup_color_background) + )?; + } + false => { + queue!( + self.ostream, + SetForegroundColor(Color::Reset), + SetBackgroundColor(Color::Reset) + )?; + } + } + ' ' + } + x => { + queue!(self.ostream, SetForegroundColor(Color::Reset)).unwrap_or(()); + queue!(self.ostream, SetBackgroundColor(Color::Reset)).unwrap_or(()); + (x + b'0') as char + } + }; + write!(self.ostream, "{}", chr)?; } + + self.move_cursor_by(-24, 0)?; } + Ok(()) } - fn render_scoreboard_elements(&mut self, state: &State) { - // char indeces of scoreboard elements - // ----- - // - // difficulty [1][34] - [1][38] - // completion [2][34] - [1][35] - // - // timer [4][34] - [3][38] - // - // > edit: [6][33] - // > markup: [7][33] - // > go: [8][33] - // - // preselection: [10][36] - // preselection completion: [11][34] + fn draw_scoreboard(&mut self, state: &State) -> io::Result<()> { + self.init_cursor_offset()?; + queue!(self.ostream, SetForegroundColor(Color::Reset))?; + queue!(self.ostream, SetBackgroundColor(Color::Reset))?; - for (i, c) in (34..39).zip(state.get_difficulty_chars()) { - self.data[1][i] = c; + self.move_cursor_by(34, 1)?; + write!(self.ostream, "{}", state.get_difficulty_string())?; + + self.move_cursor_by(-5, 1)?; + queue!(self.ostream, SetForegroundColor(self.presel_color_pair.1))?; + write!(self.ostream, "{}", state.get_completion_string())?; + + self.move_cursor_by(-2, 2)?; + let (mins_string, secs_string) = state.get_timer_strings(); + write!(self.ostream, "{}", mins_string)?; + self.move_cursor_by(1, 0)?; + write!(self.ostream, "{}", secs_string)?; + queue!(self.ostream, SetForegroundColor(Color::Reset))?; + + self.move_cursor_by(-6, 2)?; + for _ in 0..3 { + write!(self.ostream, " ")?; + self.move_cursor_by(-1, 1)?; } - for (i, c) in (34..36).zip(state.get_completion_chars()) { - self.data[2][i] = c; + self.move_cursor_by(0, -3)?; + + let selected_mode_idx = match state.mode { + Mode::Edit => 0, + Mode::Markup => 1, + Mode::Go => 2, + }; + + queue!(self.ostream, SetForegroundColor(self.presel_color_pair.1))?; + for i in 0..3 { + if i == selected_mode_idx { + write!(self.ostream, ">")?; + } + self.move_cursor_by(0, 1)?; } - for (i, c) in (34..39).zip(state.get_timer_chars()) { - self.data[4][i] = c; - } + self.move_cursor_by(2, 1)?; + write!(self.ostream, "{}", (state.preselection + b'0') as char)?; - for i in 6..9 { - self.data[i][33] = ' '; - } + self.move_cursor_by(-3, 1)?; + write!(self.ostream, "{}", state.get_preselection_completion_char())?; - self.data[match state.mode { - Mode::Edit => 6, - Mode::Markup => 7, - Mode::Go => 8, - }][33] = '>'; - - self.data[10][36] = (state.preselection + b'0') as char; - - self.data[11][34] = state.get_preselection_completion_char(); + queue!(self.ostream, SetForegroundColor(Color::Reset))?; + Ok(()) } - pub fn draw_cursor(&mut self, row: usize, col: usize) -> io::Result<()> { + fn draw_cursor(&mut self, state: &State) -> io::Result<()> { + let (row, col) = (state.cur_row, state.cur_col); let (x, y) = ( (self.width / 2 - 14) as u16, (self.height / 2 - 7 + (self.height & 1)) as u16, @@ -181,72 +237,54 @@ where }; queue!(self.ostream, MoveTo(x, y), SetCursorStyle::SteadyBlock) } + + fn move_cursor_by(&mut self, x: isize, y: isize) -> io::Result<()> { + match x.cmp(&0) { + Equal => Ok(()), + Less => queue!(self.ostream, MoveLeft(-x as u16)), + Greater => queue!(self.ostream, MoveRight(x as u16)), + }?; + match y.cmp(&0) { + Equal => Ok(()), + Less => queue!(self.ostream, MoveUp(-y as u16)), + Greater => queue!(self.ostream, MoveDown(y as u16)), + }?; + Ok(()) + } + + fn init_cursor_offset(&mut self) -> io::Result<()> { + let lft_pad = (self.width / 2 - 14) as u16; + let top_pad = (self.height / 2 - 6) as u16; + queue!(self.ostream, MoveTo(lft_pad, top_pad)) + } } -const RENDER_TEMPLATE: [[char; 41]; 13] = [ +fn board_template() -> [String; 13] { [ - '┌', '─', '─', '─', '─', '─', '─', '─', '─', '┬', '─', '─', '─', '─', '─', '─', '─', '─', - '┬', '─', '─', '─', '─', '─', '─', '─', '─', '┐', ' ', ' ', ' ', ' ', '┌', '─', '─', '─', - '─', '─', '─', '─', '┐', - ], - [ - '│', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', '│', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', - '│', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', '│', ' ', ' ', ' ', ' ', '│', ' ', ' ', ' ', - ' ', ' ', ' ', ' ', '│', - ], - [ - '│', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', '│', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', - '│', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', '│', ' ', ' ', ' ', ' ', '│', ' ', ' ', ' ', - '/', '8', '1', ' ', '│', - ], - [ - '│', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', '│', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', - '│', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', '│', ' ', ' ', ' ', ' ', '├', '─', '─', '─', - '─', '─', '─', '─', '┤', - ], - [ - '├', '─', '─', '─', '─', '─', '─', '─', '─', '┼', '─', '─', '─', '─', '─', '─', '─', '─', - '┼', '─', '─', '─', '─', '─', '─', '─', '─', '┤', ' ', ' ', ' ', ' ', '│', ' ', ' ', ' ', - ' ', ' ', ' ', ' ', '│', - ], - [ - '│', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', '│', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', - '│', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', '│', ' ', ' ', ' ', ' ', '├', '─', '─', '─', - '─', '─', '─', '─', '┤', - ], - [ - '│', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', '│', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', - '│', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', '│', ' ', ' ', ' ', ' ', '│', ' ', ' ', 'E', - 'd', 'i', 't', ' ', '│', - ], - [ - '│', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', '│', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', - '│', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', '│', ' ', ' ', ' ', ' ', '│', ' ', ' ', 'M', - 'a', 'r', 'k', ' ', '│', - ], - [ - '├', '─', '─', '─', '─', '─', '─', '─', '─', '┼', '─', '─', '─', '─', '─', '─', '─', '─', - '┼', '─', '─', '─', '─', '─', '─', '─', '─', '┤', ' ', ' ', ' ', ' ', '│', ' ', ' ', 'G', - 'o', ' ', ' ', ' ', '│', - ], - [ - '│', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', '│', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', - '│', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', '│', ' ', ' ', ' ', ' ', '├', '─', '─', '─', - '─', '─', '─', '─', '┤', - ], - [ - '│', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', '│', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', - '│', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', '│', ' ', ' ', ' ', ' ', '│', ' ', ' ', '[', - ' ', ']', ' ', ' ', '│', - ], - [ - '│', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', '│', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', - '│', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', '│', ' ', ' ', ' ', ' ', '│', ' ', ' ', ' ', - '/', ' ', '9', ' ', '│', - ], - [ - '└', '─', '─', '─', '─', '─', '─', '─', '─', '┴', '─', '─', '─', '─', '─', '─', '─', '─', - '┴', '─', '─', '─', '─', '─', '─', '─', '─', '┘', ' ', ' ', ' ', ' ', '└', '─', '─', '─', - '─', '─', '─', '─', '┘', - ], -]; + String::from("┌────────┬────────┬────────┐ ┌───────┐"), + String::from("│ │ │ │ │ │"), + String::from("│ │ │ │ │ /81 │"), + String::from("│ │ │ │ ├───────┤"), + String::from("├────────┼────────┼────────┤ │ : │"), + String::from("│ │ │ │ ├───────┤"), + String::from("│ │ │ │ │ Edit │"), + String::from("│ │ │ │ │ Mark │"), + String::from("├────────┼────────┼────────┤ │ Go │"), + String::from("│ │ │ │ ├───────┤"), + String::from("│ │ │ │ │ [ ] │"), + String::from("│ │ │ │ │ / 9 │"), + String::from("└────────┴────────┴────────┘ └───────┘"), + ] +} + +pub trait UiCrash { + fn or_crash(&self); +} + +impl UiCrash for io::Result { + fn or_crash(&self) { + if self.is_err() { + std::process::exit(1); + } + } +}