Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

scrolling #7

Closed
wants to merge 15 commits into from
4 changes: 3 additions & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ use tui::{
backend::{Backend, CrosstermBackend},
Frame, Terminal,
};
use ui::HORIZONTAL_MARGIN;
use webbrowser::Browser;

const TICK_RATE_MS: u64 = 100;
Expand Down Expand Up @@ -177,7 +178,6 @@ fn start_tui<B: Backend>(
loop {
let mut exit_type: ExitType = ExitType::Quit;
terminal.draw(|f| ui(app, f))?;

loop {
let app = &mut app;

Expand Down Expand Up @@ -307,5 +307,7 @@ fn get_thok_events(should_tick: bool) -> mpsc::Receiver<ThokEvent> {
}

fn ui<B: Backend>(app: &mut App, f: &mut Frame<B>) {
app.thok
.scroll_if_line_exhausted((f.size().width - HORIZONTAL_MARGIN * 2).into());
f.render_widget(&app.thok, f.size());
}
85 changes: 85 additions & 0 deletions src/thok.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use crate::TICK_RATE_MS;
use chrono::prelude::*;
use directories::ProjectDirs;
use itertools::Itertools;
use std::collections::HashSet;
use std::fs::OpenOptions;
use std::io::{self, Write};
use std::{char, collections::HashMap, time::SystemTime};
Expand Down Expand Up @@ -35,6 +36,11 @@ pub struct Thok {
pub wpm: f64,
pub accuracy: f64,
pub std_dev: f64,
pub line_lengths: Vec<usize>,
pub total_line_length: usize,
pub skip_curr: usize,
pub current_line: usize,
pub endpoints: HashSet<usize>,
}

impl Thok {
Expand All @@ -52,6 +58,11 @@ impl Thok {
wpm: 0.0,
accuracy: 0.0,
std_dev: 0.0,
line_lengths: vec![],
total_line_length: 0,
skip_curr: 0,
current_line: 0,
endpoints: HashSet::new(),
}
}

Expand Down Expand Up @@ -153,6 +164,21 @@ impl Thok {
pub fn backspace(&mut self) {
if self.cursor_pos > 0 {
self.input.remove(self.cursor_pos - 1);
// if the user is on an endpoint
if self.endpoints.contains(&self.cursor_pos) {
// and we have already skipped a line
if self.skip_curr > 0 {
// we need to add another line to the view
// so we "unskip" it
self.skip_curr -= self.line_lengths[self.current_line - 2];
}
// line changed so decrease the total line length and the current line
self.total_line_length -= self.line_lengths.pop().unwrap();
self.current_line -= 1;

// and finally remove it from the set
self.endpoints.remove(&self.cursor_pos);
}
self.decrement_cursor();
}
}
Expand Down Expand Up @@ -234,4 +260,63 @@ impl Thok {

Ok(())
}

// this is a helper function which decides if we should scroll to the next line
// it is called on every keystroke and it calculates the length of the next word
// to determine if it can fit on the same line. If it can't, it means we have moved onto the
// next line and so we attempt to scroll - that is achieved by the scroll_line function
pub fn scroll_if_line_exhausted(&mut self, max_width: usize) {
let count = self.cursor_pos - self.total_line_length;

// this is a special case when there's a single word which spans multiple lines
// since there is no space, the check at line 283 fails.
if count == max_width {
// user is now on a new line
// so scroll
self.scroll_line(count);
return;
}

// this is the case when the current position is <= the max width
// but the next word might not fit and may be on the next line
// first, we remove everything up to the current position
let rest = &self.prompt[self.cursor_pos..];

// then we find the next space
// which helps us in finding the length of the next word
let index = rest.find(' ');
if let Some(index) = index {
let next_word = &rest[..index];
let next_word_len = next_word.len();
// if the next word can't fit on the current line
if count + next_word_len > max_width {
// user is now on a new line
// so scroll
self.scroll_line(count);
}
}
}

// this function is responsible for scrolling to the next line
// if the number of typed lines is less than 2, then this does nothing
// if its greater, its updates the skip_count so that the 2nd last typed line
// is no longer visible
fn scroll_line(&mut self, line_length: usize) {
// line_length is the actual number of characters in this line
// we push it on the Vector
self.line_lengths.push(line_length);
self.total_line_length += line_length;

// this stores the endpoint of each line
// when the user hits backspace, its used to check if we should go back to a previous line
self.endpoints.insert(self.total_line_length);
self.current_line += 1;

// we start skipping lines when the number of lines completed is >= 2
if self.line_lengths.len() >= 2 {
// we always have a delay of 2 - if you're on line 3, then we must skip line
// 3 - 2, that is line 1
self.skip_curr += self.line_lengths[self.current_line - 2];
}
}
}
27 changes: 16 additions & 11 deletions src/ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use webbrowser::Browser;

use crate::thok::{Outcome, Thok};

const HORIZONTAL_MARGIN: u16 = 5;
pub const HORIZONTAL_MARGIN: u16 = 5;
const VERTICAL_MARGIN: u16 = 2;

impl Widget for &Thok {
Expand All @@ -36,39 +36,44 @@ impl Widget for &Thok {
match !self.has_finished() {
true => {
let max_chars_per_line = area.width - (HORIZONTAL_MARGIN * 2);
let mut prompt_occupied_lines =
((self.prompt.width() as f64 / max_chars_per_line as f64).ceil() + 1.0) as u16;
let prompt_occupied_lines = if self.prompt.width() <= max_chars_per_line as usize {
1
} else {
3
};

let time_left_lines = if self.number_of_secs.is_some() { 2 } else { 0 };

if self.prompt.width() <= max_chars_per_line as usize {
prompt_occupied_lines = 1;
}

let chunks = Layout::default()
.direction(Direction::Vertical)
.horizontal_margin(HORIZONTAL_MARGIN)
.constraints(
[
Constraint::Length(
((area.height as f64 - prompt_occupied_lines as f64) / 2.0) as u16,
((area.height as f64
- prompt_occupied_lines as f64
- time_left_lines as f64)
/ 2.0) as u16,
),
Constraint::Length(time_left_lines),
Constraint::Length(prompt_occupied_lines),
Constraint::Length(
((area.height as f64 - prompt_occupied_lines as f64) / 2.0) as u16,
((area.height as f64
- prompt_occupied_lines as f64
- time_left_lines as f64)
/ 2.0) as u16,
),
]
.as_ref(),
)
.split(area);

let mut spans = self
.input
.iter()
.skip(self.skip_curr)
.enumerate()
.map(|(idx, input)| {
let expected = self.get_expected_char(idx).to_string();
let expected = self.get_expected_char(self.skip_curr + idx).to_string();

match input.outcome {
Outcome::Incorrect => Span::styled(
Expand Down