add undo/redo functionality; organize and comment
This commit is contained in:
parent
e8b47f31ac
commit
6424063250
@ -4,13 +4,9 @@ use crossterm::event::{poll, read, Event::*, KeyCode::*};
|
||||
extern crate rand;
|
||||
|
||||
mod state;
|
||||
use state::*;
|
||||
|
||||
mod sudoku;
|
||||
use sudoku::*;
|
||||
|
||||
mod ui;
|
||||
use ui::*;
|
||||
use {state::*, sudoku::*, ui::*};
|
||||
|
||||
use std::{io, time::Duration};
|
||||
|
||||
@ -81,6 +77,9 @@ fn main() {
|
||||
_ => state.preselect_num(num as u8 - b'0'),
|
||||
},
|
||||
|
||||
Char('u') | Char('U') => state.undo(),
|
||||
Char('r') | Char('R') => state.redo(),
|
||||
|
||||
Char('q') | Char('Q') => {
|
||||
screen.deinit().or_crash();
|
||||
break;
|
||||
|
||||
270
src/state.rs
270
src/state.rs
@ -3,62 +3,57 @@ use crate::Dir::*;
|
||||
|
||||
use std::time;
|
||||
|
||||
pub enum Dir {
|
||||
Up,
|
||||
Down,
|
||||
Left,
|
||||
Right,
|
||||
FarUp,
|
||||
FarDown,
|
||||
FarLeft,
|
||||
FarRight,
|
||||
}
|
||||
|
||||
#[derive(Default, PartialEq, Clone, Copy)]
|
||||
pub enum Mode {
|
||||
#[default]
|
||||
Edit,
|
||||
Markup,
|
||||
Go,
|
||||
}
|
||||
|
||||
/// the entire game logic state
|
||||
pub struct State {
|
||||
pub board: Board,
|
||||
pub difficulty: Difficulty,
|
||||
pub modifiable: [[bool; 9]; 9],
|
||||
pub markups: [[[bool; 9]; 9]; 9],
|
||||
pub start_time: time::Instant,
|
||||
pub mode: Mode,
|
||||
pub next_mode: Mode,
|
||||
|
||||
pub preselection: u8,
|
||||
pub cur_row: usize,
|
||||
pub cur_col: usize,
|
||||
|
||||
pub mode: Mode,
|
||||
pub next_mode: Mode,
|
||||
|
||||
pub difficulty: Difficulty,
|
||||
pub start_time: time::Instant,
|
||||
|
||||
pub undo_stack: Vec<UndoStep>,
|
||||
pub redo_stack: Vec<UndoStep>,
|
||||
}
|
||||
|
||||
impl State {
|
||||
/// returns a new `State` with a randomly generated
|
||||
/// sudoku `Board` of the provided `Difficulty`
|
||||
pub fn init(difficulty: Difficulty) -> Self {
|
||||
let board = generate_sudoku(difficulty);
|
||||
let modifiable = State::get_modifiables(board);
|
||||
let modifiable = State::init_modifiables(board);
|
||||
|
||||
Self {
|
||||
board,
|
||||
difficulty,
|
||||
modifiable,
|
||||
markups: [[[false; 9]; 9]; 9],
|
||||
start_time: time::Instant::now(),
|
||||
mode: Mode::default(),
|
||||
next_mode: Mode::default(),
|
||||
|
||||
preselection: 1,
|
||||
cur_row: 4,
|
||||
cur_col: 4,
|
||||
|
||||
mode: Mode::default(),
|
||||
next_mode: Mode::default(),
|
||||
|
||||
difficulty,
|
||||
start_time: time::Instant::now(),
|
||||
|
||||
undo_stack: Vec::with_capacity(160),
|
||||
redo_stack: Vec::with_capacity(80),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_time(&self) -> time::Duration {
|
||||
time::Instant::now() - self.start_time
|
||||
}
|
||||
|
||||
fn get_modifiables(board: Board) -> [[bool; 9]; 9] {
|
||||
/// returns a boolean mask of the board, indicating which cells
|
||||
/// can be modified by the user and which are part of the puzzle constraints.
|
||||
/// makes only cells that are initialized with the value 0 modifiable
|
||||
fn init_modifiables(board: Board) -> [[bool; 9]; 9] {
|
||||
let mut modifiable = [[false; 9]; 9];
|
||||
for (board_row, modifiable_row) in board.iter().zip(modifiable.iter_mut()) {
|
||||
for (board_cell, modifiable_flag) in board_row.iter().zip(modifiable_row.iter_mut()) {
|
||||
@ -74,6 +69,8 @@ impl State {
|
||||
self.modifiable[self.cur_row][self.cur_col]
|
||||
}
|
||||
|
||||
/// returns a mutable reference to the number
|
||||
/// contained in the current cell.
|
||||
pub fn current_cell(&mut self) -> &mut u8 {
|
||||
&mut self.board[self.cur_row][self.cur_col]
|
||||
}
|
||||
@ -103,7 +100,7 @@ impl State {
|
||||
pub fn toggle_current_cell(&mut self) {
|
||||
if self.current_cell_is_modifiable() {
|
||||
if *self.current_cell() == self.preselection {
|
||||
*self.current_cell() = 0;
|
||||
self.delete_current_cell();
|
||||
} else {
|
||||
*self.current_cell() = self.preselection;
|
||||
self.delete_colliding_marks(self.preselection, self.cur_row, self.cur_col);
|
||||
@ -114,15 +111,91 @@ impl State {
|
||||
pub fn delete_current_cell(&mut self) {
|
||||
if self.current_cell_is_modifiable() {
|
||||
*self.current_cell() = 0;
|
||||
|
||||
self.push_to_undo_stack(UndoStep::Replace(
|
||||
self.preselection,
|
||||
(self.cur_row as u8, self.cur_col as u8),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// deletes all marks of `num` in it's row, column and block.
|
||||
/// used to automatically remove marks when placing a number that
|
||||
/// invalidates those marks.
|
||||
pub fn delete_colliding_marks(&mut self, num: u8, row: usize, col: usize) {
|
||||
let mut deleted = [None; 27];
|
||||
let mut pos = 0;
|
||||
|
||||
for i in 0..9 {
|
||||
if self.markups[i][col][num as usize - 1] {
|
||||
deleted[pos] = Some((i as u8, col as u8));
|
||||
pos += 1;
|
||||
}
|
||||
if self.markups[row][i][num as usize - 1] {
|
||||
deleted[pos] = Some((row as u8, i as u8));
|
||||
pos += 1;
|
||||
}
|
||||
if self.markups[row / 3 * 3 + i / 3][col / 3 * 3 + i % 3][num as usize - 1] {
|
||||
deleted[pos] = Some((
|
||||
row as u8 / 3 * 3 + i as u8 / 3,
|
||||
col as u8 / 3 * 3 + i as u8 % 3,
|
||||
));
|
||||
pos += 1;
|
||||
}
|
||||
|
||||
self.markups[i][col][num as usize - 1] = false;
|
||||
self.markups[row][i][num as usize - 1] = false;
|
||||
self.markups[row / 3 * 3 + i / 3][col / 3 * 3 + i % 3][num as usize - 1] = false;
|
||||
}
|
||||
|
||||
self.push_to_undo_stack(UndoStep::Unplace(
|
||||
self.preselection,
|
||||
(row as u8, col as u8),
|
||||
deleted,
|
||||
));
|
||||
}
|
||||
|
||||
pub fn toggle_current_mark(&mut self) {
|
||||
if self.markups[self.cur_row][self.cur_col][self.preselection as usize - 1] {
|
||||
self.delete_current_mark();
|
||||
} else {
|
||||
self.set_current_mark();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn delete_current_mark(&mut self) {
|
||||
if *self.current_cell() != 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
self.markups[self.cur_row][self.cur_col][self.preselection as usize - 1] = false;
|
||||
|
||||
self.push_to_undo_stack(UndoStep::Remark(
|
||||
self.preselection,
|
||||
(self.cur_row as u8, self.cur_col as u8),
|
||||
));
|
||||
}
|
||||
|
||||
pub fn set_current_mark(&mut self) {
|
||||
if *self.current_cell() != 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
self.markups[self.cur_row][self.cur_col][self.preselection as usize - 1] = true;
|
||||
|
||||
self.push_to_undo_stack(UndoStep::Unmark(
|
||||
self.preselection,
|
||||
(self.cur_row as u8, self.cur_col as u8),
|
||||
));
|
||||
}
|
||||
|
||||
pub fn move_cursor_to(&mut self, row: usize, col: usize) {
|
||||
assert!(
|
||||
row < 9 && col < 9,
|
||||
"[!] Error: State::go_to: Can't move to row/column out of bounds."
|
||||
);
|
||||
self.cur_row = row;
|
||||
self.cur_col = col;
|
||||
}
|
||||
|
||||
pub fn enter_mode(&mut self, mode: Mode) {
|
||||
@ -137,6 +210,8 @@ impl State {
|
||||
self.next_mode = self.mode;
|
||||
}
|
||||
|
||||
/// enter a mode until one action has been taken,
|
||||
/// then returning to the previous mode
|
||||
pub fn enter_mode_once(&mut self, mode: Mode) {
|
||||
if self.mode != mode {
|
||||
self.next_mode = self.mode;
|
||||
@ -148,23 +223,11 @@ impl State {
|
||||
self.mode = self.next_mode;
|
||||
}
|
||||
|
||||
pub fn move_cursor_to(&mut self, row: usize, col: usize) {
|
||||
assert!(
|
||||
row < 9 && col < 9,
|
||||
"[!] Error: State::go_to: Can't move to row/column out of bounds."
|
||||
);
|
||||
self.cur_row = row;
|
||||
self.cur_col = col;
|
||||
}
|
||||
|
||||
pub fn toggle_current_mark(&mut self) {
|
||||
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 - 1] = false;
|
||||
fn get_elapsed_time(&self) -> time::Duration {
|
||||
time::Instant::now() - self.start_time
|
||||
}
|
||||
|
||||
/// returns number of filled cells
|
||||
pub fn get_completion_string(&self) -> String {
|
||||
let mut count = 0;
|
||||
for row in self.board {
|
||||
@ -178,6 +241,9 @@ impl State {
|
||||
[to_char(count / 10), to_char(count % 10)].iter().collect()
|
||||
}
|
||||
|
||||
/// returns how many of the 9 final occurences of the
|
||||
/// preselected number have been found.
|
||||
/// returns `'!'` if the user erroneously placed more than 9.
|
||||
pub fn get_preselection_completion_char(&self) -> char {
|
||||
let count = self
|
||||
.board
|
||||
@ -185,9 +251,14 @@ impl State {
|
||||
.flatten()
|
||||
.filter(|&cell| cell == self.preselection)
|
||||
.count();
|
||||
(count.min(9) as u8 + b'0') as char
|
||||
match count {
|
||||
10.. => '!',
|
||||
_ => (count as u8 + b'0') as char,
|
||||
}
|
||||
}
|
||||
|
||||
/// returns the difficulty string used on the ingame scoreboard.
|
||||
/// if you want the complete difficulty names use `Difficulty::to_string()`
|
||||
pub fn get_difficulty_string(&self) -> String {
|
||||
match self.difficulty {
|
||||
Difficulty::Easy => String::from("Easy "),
|
||||
@ -198,8 +269,10 @@ impl State {
|
||||
}
|
||||
}
|
||||
|
||||
/// returns (mins, secs) as a pair of strings
|
||||
/// both are zero-padded to a width of 2 characters
|
||||
pub fn get_timer_strings(&self) -> (String, String) {
|
||||
let mut n = self.get_time().as_secs();
|
||||
let mut n = self.get_elapsed_time().as_secs();
|
||||
|
||||
let mut mins = String::with_capacity(2);
|
||||
let mut secs = String::with_capacity(2);
|
||||
@ -218,6 +291,7 @@ impl State {
|
||||
(mins, secs)
|
||||
}
|
||||
|
||||
/// returns timer as string in `mm:ss` format
|
||||
pub fn get_timer_string(&self) -> String {
|
||||
let (mins, secs) = self.get_timer_strings();
|
||||
let mut timer = String::with_capacity(5);
|
||||
@ -226,4 +300,98 @@ impl State {
|
||||
timer.push_str(&secs);
|
||||
timer
|
||||
}
|
||||
|
||||
/// undo an action that has been taken
|
||||
pub fn undo(&mut self) {
|
||||
use UndoStep::*;
|
||||
|
||||
if let Some(undo_step) = self.undo_stack.pop() {
|
||||
self.redo_stack.push(undo_step);
|
||||
match undo_step {
|
||||
Unplace(num, (row, col), deleted_marks) => {
|
||||
self.board[row as usize][col as usize] = 0;
|
||||
for (r, c) in deleted_marks.iter().take_while(|x| x.is_some()).flatten() {
|
||||
self.markups[*r as usize][*c as usize][num as usize - 1] = true;
|
||||
}
|
||||
}
|
||||
Replace(num, (row, col)) => {
|
||||
self.board[row as usize][col as usize] = num;
|
||||
}
|
||||
Remark(num, (row, col)) => {
|
||||
self.markups[row as usize][col as usize][num as usize - 1] = true;
|
||||
}
|
||||
Unmark(num, (row, col)) => {
|
||||
self.markups[row as usize][col as usize][num as usize - 1] = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// redo an action if one was taken and undone.
|
||||
pub fn redo(&mut self) {
|
||||
use UndoStep::*;
|
||||
|
||||
if let Some(redo_step) = self.redo_stack.pop() {
|
||||
self.undo_stack.push(redo_step);
|
||||
match redo_step {
|
||||
Unplace(num, (row, col), deleted_marks) => {
|
||||
self.board[row as usize][col as usize] = num;
|
||||
for (r, c) in deleted_marks.iter().take_while(|x| x.is_some()).flatten() {
|
||||
self.markups[*r as usize][*c as usize][num as usize - 1] = false;
|
||||
}
|
||||
}
|
||||
Replace(_, (row, col)) => {
|
||||
self.board[row as usize][col as usize] = 0;
|
||||
}
|
||||
Remark(num, (row, col)) => {
|
||||
self.markups[row as usize][col as usize][num as usize - 1] = false;
|
||||
}
|
||||
Unmark(num, (row, col)) => {
|
||||
self.markups[row as usize][col as usize][num as usize - 1] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// pushes a move done by the player to the undo stack
|
||||
/// this additionally invalidates the redo stack
|
||||
pub fn push_to_undo_stack(&mut self, undo_step: UndoStep) {
|
||||
self.undo_stack.push(undo_step);
|
||||
self.redo_stack.clear();
|
||||
}
|
||||
}
|
||||
|
||||
pub enum Dir {
|
||||
Up,
|
||||
Down,
|
||||
Left,
|
||||
Right,
|
||||
FarUp,
|
||||
FarDown,
|
||||
FarLeft,
|
||||
FarRight,
|
||||
}
|
||||
|
||||
#[derive(Default, PartialEq, Clone, Copy)]
|
||||
pub enum Mode {
|
||||
#[default]
|
||||
Edit,
|
||||
Markup,
|
||||
Go,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone, Copy)]
|
||||
pub enum UndoStep {
|
||||
/// unplace num at (row, col) and re-mark
|
||||
/// (row, col) if there were removed marks
|
||||
Unplace(u8, (u8, u8), [Option<(u8, u8)>; 27]),
|
||||
|
||||
/// re-place num at (row, col)
|
||||
Replace(u8, (u8, u8)),
|
||||
|
||||
/// unmark num at (row, col)
|
||||
Unmark(u8, (u8, u8)),
|
||||
|
||||
/// re-mark num at (row, col)
|
||||
Remark(u8, (u8, u8)),
|
||||
}
|
||||
|
||||
@ -1,6 +1,3 @@
|
||||
// TODO: remove when done
|
||||
#![cfg_attr(debug_assertions, allow(unused))]
|
||||
|
||||
use crate::generator::Difficulty::*;
|
||||
use crate::rand::{seq::SliceRandom, thread_rng};
|
||||
use crate::sudoku::{Board, New};
|
||||
@ -9,6 +6,7 @@ use std::fmt;
|
||||
|
||||
/// categories of difficulty, indicating how many
|
||||
/// empty spaces will be on a sudoku board.
|
||||
/// (see `Difficulty::removal_count()` for values)
|
||||
#[allow(unused)]
|
||||
#[derive(Default, Copy, Clone, PartialEq)]
|
||||
pub enum Difficulty {
|
||||
@ -47,6 +45,8 @@ impl fmt::Display for Difficulty {
|
||||
}
|
||||
|
||||
/// generate a random, unsolved sudoku board with a given `Difficulty`.
|
||||
/// solves a empty sudoku with random cell order and then removes
|
||||
/// some number of cells depending on the difficulty.
|
||||
pub fn generate_sudoku(difficulty: Difficulty) -> Board {
|
||||
let mut board = Board::new();
|
||||
|
||||
@ -65,13 +65,15 @@ pub fn generate_sudoku(difficulty: Difficulty) -> Board {
|
||||
board
|
||||
}
|
||||
|
||||
/// solves a sudoku, randomizing which empty cell of equal
|
||||
/// mrv to choose next.
|
||||
/// this is used to generate random, fully solvable sudokus.
|
||||
///
|
||||
/// NOTE: this does not guarantee a single-solution sudoku.
|
||||
///
|
||||
/// TODO: currently empty cells needs to be recreated
|
||||
/// on each recursive call, and are randomized anew
|
||||
/// each time, which is an unnecessary overhead.
|
||||
///
|
||||
/// solves a sudoku, randomizing which empty cell of equal
|
||||
/// mrv () to choose next.
|
||||
/// this is used to generate random, fully solvable sudokus.
|
||||
fn solve_random(board: &mut Board) -> Result<(), ()> {
|
||||
let mut empty_cells: Vec<(usize, usize)> = Vec::new();
|
||||
|
||||
|
||||
@ -11,6 +11,7 @@ trait New {
|
||||
}
|
||||
|
||||
impl New for Board {
|
||||
/// return an empty `Board`
|
||||
fn new() -> Self {
|
||||
[[0u8; 9]; 9]
|
||||
}
|
||||
|
||||
@ -1,8 +1,6 @@
|
||||
// TODO: remove when done
|
||||
#![cfg_attr(debug_assertions, allow(unused))]
|
||||
|
||||
use crate::sudoku::Board;
|
||||
|
||||
/// returns true if the provided sudoku `Board` is in a solved state
|
||||
pub fn is_solution(sudoku: &Board) -> bool {
|
||||
for i in 0..9 {
|
||||
let mut row_set = 0u16;
|
||||
|
||||
66
src/ui.rs
66
src/ui.rs
@ -16,7 +16,8 @@ use crossterm::{
|
||||
},
|
||||
};
|
||||
|
||||
pub struct Screen<T>
|
||||
/// state of the user interface
|
||||
pub struct Ui<T>
|
||||
where
|
||||
T: io::Write,
|
||||
{
|
||||
@ -27,10 +28,15 @@ where
|
||||
pub height: usize,
|
||||
}
|
||||
|
||||
impl<T> Screen<T>
|
||||
impl<T> Ui<T>
|
||||
where
|
||||
T: io::Write,
|
||||
{
|
||||
/// returns a new `Ui` using `ostream` as its' output stream
|
||||
/// and sets up terminal state:
|
||||
/// - remembers the current cursor position
|
||||
/// - enters alternate screen buffer
|
||||
/// - enables raw mode
|
||||
pub fn init(ostream: T) -> Self {
|
||||
let (width, height) = size().unwrap();
|
||||
let (width, height) = (width as usize, height as usize);
|
||||
@ -38,7 +44,7 @@ where
|
||||
let presel_color_pair = (Color::Black, Color::Cyan);
|
||||
let markup_color_background = Color::Cyan;
|
||||
|
||||
let mut screen = Screen {
|
||||
let mut ui = Ui {
|
||||
presel_color_pair,
|
||||
markup_color_background,
|
||||
ostream,
|
||||
@ -47,22 +53,29 @@ where
|
||||
};
|
||||
|
||||
queue!(
|
||||
screen.ostream,
|
||||
ui.ostream,
|
||||
SavePosition,
|
||||
EnterAlternateScreen,
|
||||
Clear(All)
|
||||
)
|
||||
.expect("[-]: Error: ui::init: Failed to enter alternate screen.");
|
||||
enable_raw_mode().expect("[-]: Error: ui::init: Failed to enable raw mode.");
|
||||
screen
|
||||
ui
|
||||
}
|
||||
|
||||
/// resets terminal state that `Ui::init()` sets:
|
||||
/// - disables raw mode
|
||||
/// - leaves alternate screen buffer
|
||||
/// - restores cursor position
|
||||
pub fn deinit(&mut self) -> io::Result<()> {
|
||||
disable_raw_mode()?;
|
||||
execute!(self.ostream, LeaveAlternateScreen, RestorePosition)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// updates `width` and `height`.
|
||||
/// clears the screen and redraws the board if the dimensions changed.
|
||||
/// returns an error if the new dimensions are too small to fit the ui.
|
||||
pub fn update_dimensions(&mut self) -> io::Result<()> {
|
||||
let old_dimensions = (self.width, self.height);
|
||||
|
||||
@ -90,6 +103,15 @@ where
|
||||
queue!(self.ostream, MoveToColumn(0))
|
||||
}
|
||||
|
||||
/// draws the entire ui, watching for possibly changed screen dimensions.
|
||||
/// all other `draw_...()` functions just `queue!(...)` the actions to draw
|
||||
/// their respective ui element.
|
||||
/// this function chains all of these calls in order and then flushes the
|
||||
/// changes to `self.ostream`.
|
||||
///
|
||||
/// NOTE: this function does not draw the static elements of the ui
|
||||
/// using `Ui::draw_board()`. for efficiency these are only
|
||||
/// redrawn on screen dimensions changes.
|
||||
pub fn draw(&mut self, state: &State) -> io::Result<()> {
|
||||
self.update_dimensions()?;
|
||||
|
||||
@ -100,6 +122,12 @@ where
|
||||
self.ostream.flush()
|
||||
}
|
||||
|
||||
/// `queue!(...)`s the drawing of the unchanging ui elements
|
||||
/// such as the board and scoreboards outlines.
|
||||
/// uses the lines provided by `board_template()`.
|
||||
///
|
||||
/// NOTE: this function itself does not flush to `self.ostream`
|
||||
/// in order to only have to flush once per frame.
|
||||
pub fn draw_board(&mut self) -> io::Result<()> {
|
||||
self.init_cursor_offset()?;
|
||||
queue!(self.ostream, SetForegroundColor(Color::Reset))?;
|
||||
@ -114,6 +142,10 @@ where
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// `queue!(...)`s the drawing of the numbers in the cells.
|
||||
///
|
||||
/// NOTE: this function itself does not flush to `self.ostream`
|
||||
/// in order to only have to flush once per frame.
|
||||
fn draw_numbers(&mut self, state: &State) -> io::Result<()> {
|
||||
self.init_cursor_offset()?;
|
||||
queue!(self.ostream, SetForegroundColor(Color::Reset))?;
|
||||
@ -174,6 +206,10 @@ where
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// `queue!(...)`s the drawing of variable scoreboard content
|
||||
///
|
||||
/// NOTE: this function itself does not flush to `self.ostream`
|
||||
/// in order to only have to flush once per frame.
|
||||
fn draw_scoreboard(&mut self, state: &State) -> io::Result<()> {
|
||||
self.init_cursor_offset()?;
|
||||
queue!(self.ostream, SetForegroundColor(Color::Reset))?;
|
||||
@ -225,6 +261,10 @@ where
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// `queue!(...)`s the placement of the cursor on the selected cell.
|
||||
///
|
||||
/// NOTE: this function itself does not flush to `self.ostream`
|
||||
/// in order to only have to flush once per frame.
|
||||
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 - 6) as u16);
|
||||
@ -243,6 +283,12 @@ where
|
||||
queue!(self.ostream, MoveTo(x, y), SetCursorStyle::SteadyBlock)
|
||||
}
|
||||
|
||||
/// `queue!(...)`s the movement of the cursor by (`x`, `y`).
|
||||
/// positive `x` values correspond to movement right by `x` columns.
|
||||
/// positive `y` values correspond to movement down by `y` rows.
|
||||
///
|
||||
/// NOTE: this function itself does not flush to `self.ostream`
|
||||
/// in order to only have to flush once per frame.
|
||||
fn move_cursor_by(&mut self, x: isize, y: isize) -> io::Result<()> {
|
||||
match x.cmp(&0) {
|
||||
Equal => Ok(()),
|
||||
@ -257,6 +303,10 @@ where
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// `queue!(...)`s the movement of the cursor to the top-left
|
||||
/// of the inner ui.
|
||||
/// this simplifies handling the padding in other drawing functions
|
||||
/// since the padding depends on the screen dimensions.
|
||||
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;
|
||||
@ -264,6 +314,8 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
/// returns a template for the parts of the board that
|
||||
/// are always the same.
|
||||
fn board_template() -> [String; 13] {
|
||||
[
|
||||
String::from("┌────────┬────────┬────────┐ ┌───────┐"),
|
||||
@ -287,6 +339,10 @@ pub trait UiCrash {
|
||||
}
|
||||
|
||||
impl<T> UiCrash for io::Result<T> {
|
||||
/// ends the process with exit code 1
|
||||
/// if called on `Err(_)` variant.
|
||||
/// if called on `Ok(_)` variant, this is a no-op
|
||||
/// that discards the contained value.
|
||||
fn or_crash(&self) {
|
||||
if self.is_err() {
|
||||
std::process::exit(1);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user