reworking the reworked ui once again, added color

This commit is contained in:
markichnich 2023-08-24 15:21:32 +02:00
parent 3e9b843131
commit b2f2e0fad2
5 changed files with 236 additions and 198 deletions

View File

@ -55,7 +55,6 @@ The current control scheme adheres to vim-like keybindings and is modal.
- [x] Validate Sudokus - [x] Validate Sudokus
- [x] Generate Sudokus - [x] Generate Sudokus
- [x] Difficulties to choose - [x] Difficulties to choose
- [ ] Undo functionality
- [x] Basic UI - [x] Basic UI
@ -70,12 +69,13 @@ The current control scheme adheres to vim-like keybindings and is modal.
- [x] Preselect numbers - [x] Preselect numbers
- [x] Edit Mode to (re)place numbers - [x] Edit Mode to (re)place numbers
- [x] Markup Mode to mark where numbers could go - [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] Go Mode to move to blocks 1-9
- [x] Toggle Number/Mark with Space - [x] Toggle Number/Mark with Space
- [ ] Undo/Redo stack - [ ] Undo/Redo stack
- [ ] Colored UI - [x] Colored UI
- [ ] Hightlight preselected numbers - [x] Hightlight preselected numbers
- [ ] Hightlight preselected markups - [x] Hightlight preselected markups
- [x] Scoreboard - [x] Scoreboard
- [x] Live timer - [x] Live timer
- [x] Mode indicator - [x] Mode indicator

View File

@ -16,7 +16,8 @@ use std::{io, time::Duration};
fn main() { fn main() {
let mut screen = Screen::init(io::stdout()); 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 { loop {
if poll(Duration::from_millis(250)).unwrap_or(false) { if poll(Duration::from_millis(250)).unwrap_or(false) {
@ -49,7 +50,7 @@ fn main() {
state.toggle_current_cell(); state.toggle_current_cell();
state.enter_next_mode(); state.enter_next_mode();
if is_solution(&state.board) { if is_solution(&state.board) {
screen.deinit(); screen.deinit().or_crash();
println!("+------------+"); println!("+------------+");
println!("| You Win :) |"); println!("| You Win :) |");
println!("+------------+"); println!("+------------+");
@ -81,7 +82,7 @@ fn main() {
}, },
Char('q') | Char('Q') => { Char('q') | Char('Q') => {
screen.deinit(); screen.deinit().or_crash();
break; break;
} }
@ -91,13 +92,7 @@ fn main() {
} }
} }
screen.update_dimensions() screen.draw(&state).or_crash();
.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));
} }
} }

View File

@ -149,14 +149,14 @@ impl State {
} }
pub fn toggle_current_mark(&mut self) { 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) { 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; let mut count = 0;
for row in self.board { for row in self.board {
for cell in row { for cell in row {
@ -166,7 +166,7 @@ impl State {
} }
} }
let to_char = |x| (x + b'0') as char; 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 { pub fn get_preselection_completion_char(&self) -> char {
@ -179,37 +179,42 @@ impl State {
(count.min(9) as u8 + b'0') as char (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 { match self.difficulty {
Difficulty::Easy => [' ', 'E', 'a', 's', 'y'], Difficulty::Easy => String::from("Easy "),
Difficulty::Mid => [' ', 'M', 'i', 'd', ' '], Difficulty::Mid => String::from(" Mid "),
Difficulty::Hard => [' ', 'H', 'a', 'r', 'd'], Difficulty::Hard => String::from("Hard "),
Difficulty::Expert => ['E', 'x', 'p', 'r', 't'], Difficulty::Expert => String::from("Exprt"),
Difficulty::Custom(x) => [ Difficulty::Custom(x) => format!("C({:02})", x),
'C',
'(',
(x as u8 / 10 + b'0') as char,
(x as u8 % 10 + b'0') as char,
')',
],
} }
} }
pub fn get_timer_chars(&self) -> [char; 5] { pub fn get_timer_strings(&self) -> (String, String) {
let mut secs = self.get_time().as_secs(); let mut n = self.get_time().as_secs();
let mut chars = [':'; 5];
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; let to_char = |x: u64| (x as u8 + b'0') as char;
chars[0] = to_char(secs / 600);
secs %= 600; mins.push(to_char(n / 600));
chars[1] = to_char(secs / 60); n %= 600;
secs %= 60; mins.push(to_char(n / 60));
chars[3] = to_char(secs / 10); n %= 60;
secs %= 10;
chars[4] = to_char(secs); secs.push(to_char(n / 10));
chars n %= 10;
secs.push(to_char(n));
(mins, secs)
} }
pub fn get_timer_string(&self) -> String { 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
} }
} }

View File

@ -25,10 +25,10 @@ impl Difficulty {
/// sudoku board of the given `Difficulty` /// sudoku board of the given `Difficulty`
pub fn removal_count(&self) -> usize { pub fn removal_count(&self) -> usize {
match self { match self {
Easy => 35, Easy => 31,
Mid => 45, Mid => 39,
Hard => 52, Hard => 49,
Expert => 62, Expert => 61,
Custom(x) => *x, Custom(x) => *x,
} }
} }

342
src/ui.rs
View File

@ -1,9 +1,12 @@
use crate::state::*; use crate::state::*;
use std::cmp::Ordering::*;
use std::io; use std::io;
use crossterm::{ use crossterm::{
cursor::{MoveTo, MoveToColumn, SetCursorStyle}, cursor::{MoveDown, MoveLeft, MoveRight, MoveTo, MoveToColumn, MoveUp, SetCursorStyle},
execute, queue, execute, queue,
style::{Color, SetBackgroundColor, SetForegroundColor},
terminal::{ terminal::{
disable_raw_mode, enable_raw_mode, size, Clear, ClearType::All, EnterAlternateScreen, disable_raw_mode, enable_raw_mode, size, Clear, ClearType::All, EnterAlternateScreen,
LeaveAlternateScreen, LeaveAlternateScreen,
@ -14,7 +17,8 @@ pub struct Screen<T>
where where
T: io::Write, T: io::Write,
{ {
pub data: Vec<Vec<char>>, pub presel_color_pair: (Color, Color),
pub markup_color_background: Color,
pub ostream: T, pub ostream: T,
pub width: usize, pub width: usize,
pub height: usize, pub height: usize,
@ -28,38 +32,48 @@ where
let (width, height) = size().unwrap(); let (width, height) = size().unwrap();
let (width, height) = (width as usize, height as usize); 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 { let mut screen = Screen {
data, presel_color_pair,
markup_color_background,
ostream, ostream,
width, width,
height, height,
}; };
enable_raw_mode().expect("[!]: Error: ui::init: Failed to enable raw mode."); enable_raw_mode().expect("[-]: Error: ui::init: Failed to enable raw mode.");
queue!(screen.ostream, EnterAlternateScreen) queue!(screen.ostream, Clear(All), EnterAlternateScreen)
.expect("[!]: Error: ui::init: Failed to enter alternate screen."); .expect("[-]: Error: ui::init: Failed to enter alternate screen.");
screen screen
} }
pub fn deinit(&mut self) { pub fn deinit(&mut self) -> io::Result<()> {
disable_raw_mode().unwrap_or(()); disable_raw_mode()?;
execute!(self.ostream, LeaveAlternateScreen).unwrap_or(()); execute!(self.ostream, Clear(All), LeaveAlternateScreen)?;
Ok(())
} }
pub fn update_dimensions(&mut self) -> io::Result<()> { pub fn update_dimensions(&mut self) -> io::Result<()> {
let old_dimensions = (self.width, self.height);
let (width, height) = size()?; let (width, height) = size()?;
self.width = width as usize; self.width = width as usize;
self.height = height as usize; self.height = height as usize;
if height < 14 || width < 54 { if height < 14 || width < 54 {
self.clear()?; self.clear()?;
self.deinit(); self.deinit()?;
eprintln!("[!]: Error: ui::update_dimensions: Terminal size too small to display UI."); eprintln!("[!]: Error: ui::update_dimensions: Terminal size too small to display UI.");
return Err(io::Error::from(io::ErrorKind::Other)); return Err(io::Error::from(io::ErrorKind::Other));
} }
if old_dimensions != (self.width, self.height) {
self.clear()?;
self.draw_board()?;
}
Ok(()) Ok(())
} }
@ -68,101 +82,143 @@ where
queue!(self.ostream, MoveToColumn(0)) queue!(self.ostream, MoveToColumn(0))
} }
pub fn draw(&mut self, row: usize, col: usize) -> io::Result<()> { pub fn draw(&mut self, state: &State) -> io::Result<()> {
self.draw_screen()?; self.update_dimensions()?;
self.draw_cursor(row, col)?;
self.ostream.flush()?; 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(()) Ok(())
} }
pub fn draw_screen(&mut self) -> io::Result<()> { fn draw_numbers(&mut self, state: &State) -> io::Result<()> {
self.clear()?; self.init_cursor_offset()?;
let lft_pad = self.width / 2 - 14; queue!(self.ostream, SetForegroundColor(Color::Reset))?;
let bot_pad = self.height / 2 - 7; 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 { 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 { for col in 0..9 {
let i = row + 1 + row / 3; match col {
let j = col + 2 + col + col / 3 * 3; 3 | 6 => self.move_cursor_by(4, 0),
_ => self.move_cursor_by(1, 0),
}?;
self.data[i][j] = match state.board[row][col] { let chr = match state.board[row][col] {
0 => ' ', x if x == state.preselection => {
x => (x + b'0') as char, 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)?;
} }
fn render_scoreboard_elements(&mut self, state: &State) { self.move_cursor_by(-24, 0)?;
// char indeces of scoreboard elements }
// ----- Ok(())
//
// 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]
for (i, c) in (34..39).zip(state.get_difficulty_chars()) {
self.data[1][i] = c;
} }
for (i, c) in (34..36).zip(state.get_completion_chars()) { fn draw_scoreboard(&mut self, state: &State) -> io::Result<()> {
self.data[2][i] = c; self.init_cursor_offset()?;
queue!(self.ostream, SetForegroundColor(Color::Reset))?;
queue!(self.ostream, SetBackgroundColor(Color::Reset))?;
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..39).zip(state.get_timer_chars()) { self.move_cursor_by(0, -3)?;
self.data[4][i] = c;
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 in 6..9 { self.move_cursor_by(2, 1)?;
self.data[i][33] = ' '; write!(self.ostream, "{}", (state.preselection + b'0') as char)?;
self.move_cursor_by(-3, 1)?;
write!(self.ostream, "{}", state.get_preselection_completion_char())?;
queue!(self.ostream, SetForegroundColor(Color::Reset))?;
Ok(())
} }
self.data[match state.mode { fn draw_cursor(&mut self, state: &State) -> io::Result<()> {
Mode::Edit => 6, let (row, col) = (state.cur_row, state.cur_col);
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();
}
pub fn draw_cursor(&mut self, row: usize, col: usize) -> io::Result<()> {
let (x, y) = ( let (x, y) = (
(self.width / 2 - 14) as u16, (self.width / 2 - 14) as u16,
(self.height / 2 - 7 + (self.height & 1)) as u16, (self.height / 2 - 7 + (self.height & 1)) as u16,
@ -181,72 +237,54 @@ where
}; };
queue!(self.ostream, MoveTo(x, y), SetCursorStyle::SteadyBlock) 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] {
[ [
'┌', '─', '─', '─', '─', '─', '─', '─', '─', '┬', '─', '─', '─', '─', '─', '─', '─', '─', 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 │"),
'/', '8', '1', ' ', '│', String::from("└────────┴────────┴────────┘ └───────┘"),
], ]
[ }
'│', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', '│', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ',
'│', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', '│', ' ', ' ', ' ', ' ', '├', '─', '─', '─', pub trait UiCrash {
'─', '─', '─', '─', '┤', fn or_crash(&self);
], }
[
'├', '─', '─', '─', '─', '─', '─', '─', '─', '┼', '─', '─', '─', '─', '─', '─', '─', '─', impl<T> UiCrash for io::Result<T> {
'┼', '─', '─', '─', '─', '─', '─', '─', '─', '┤', ' ', ' ', ' ', ' ', '│', ' ', ' ', ' ', fn or_crash(&self) {
' ', ' ', ' ', ' ', '│', if self.is_err() {
], std::process::exit(1);
[ }
'│', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', '│', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', }
'│', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', '│', ' ', ' ', ' ', ' ', '├', '─', '─', '─', }
'─', '─', '─', '─', '┤',
],
[
'│', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', '│', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ',
'│', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', '│', ' ', ' ', ' ', ' ', '│', ' ', ' ', 'E',
'd', 'i', 't', ' ', '│',
],
[
'│', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', '│', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ',
'│', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', '│', ' ', ' ', ' ', ' ', '│', ' ', ' ', 'M',
'a', 'r', 'k', ' ', '│',
],
[
'├', '─', '─', '─', '─', '─', '─', '─', '─', '┼', '─', '─', '─', '─', '─', '─', '─', '─',
'┼', '─', '─', '─', '─', '─', '─', '─', '─', '┤', ' ', ' ', ' ', ' ', '│', ' ', ' ', 'G',
'o', ' ', ' ', ' ', '│',
],
[
'│', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', '│', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ',
'│', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', '│', ' ', ' ', ' ', ' ', '├', '─', '─', '─',
'─', '─', '─', '─', '┤',
],
[
'│', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', '│', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ',
'│', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', '│', ' ', ' ', ' ', ' ', '│', ' ', ' ', '[',
' ', ']', ' ', ' ', '│',
],
[
'│', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', '│', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ',
'│', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', '│', ' ', ' ', ' ', ' ', '│', ' ', ' ', ' ',
'/', ' ', '9', ' ', '│',
],
[
'└', '─', '─', '─', '─', '─', '─', '─', '─', '┴', '─', '─', '─', '─', '─', '─', '─', '─',
'┴', '─', '─', '─', '─', '─', '─', '─', '─', '┘', ' ', ' ', ' ', ' ', '└', '─', '─', '─',
'─', '─', '─', '─', '┘',
],
];