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

improve help window ux #72

Merged
merged 2 commits into from
Nov 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 79 additions & 21 deletions src/help_window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,45 +13,103 @@ impl HelpWindow {
HelpWindow
}

pub fn render(&self, frame: &mut impl FrameWrapperInterface, area: Rect) {
// Clear the background area first.
frame.render_widget(Clear, area);
pub fn render(&self, frame: &mut impl FrameWrapperInterface) {
let frame_rect = frame.size();

let block = Block::default()
.title("Help")
.borders(Borders::ALL)
.style(Style::default().fg(Color::White).bg(Color::Black));

let inner_area = block.inner(area);
if frame_rect.height < 3 {
frame.render_widget(Clear, frame_rect);
return;
}

let help_text = vec![
"Navigation:",
"s - Start/unpause the test",
"Esc - Pause the test",
"q - Quit",
"",
"Configuration:",
"--duration <seconds> - Set test duration",
"--numbers - Include numbers in the test",
"--uppercase - Include uppercase letters",
"Run 'donkeytype help' for more options",
" Navigation:",
" 's' - Start/resume the test",
" <Esc> - Pause the test",
" 'q' - Quit",
" '?' - Toggle this window",
"",
" Configuration:",
" --duration <seconds> - Set test duration",
" --numbers - Include numbers in the test",
" --uppercase - Include uppercase letters",
"",
" Run 'donkeytype help' in your terminal to get more information ",
"",
];

let longest_help_msg_len = help_text.iter().map(|s| s.len()).max().unwrap();
let help_text_lines_count = help_text.len();

// check if there is enough space vertically to display the help message
if frame_rect.height <= help_text_lines_count as u16 {
let paragraph =
Paragraph::new( "Terminal window is too short to display the help window\nresize the terminal or press \"?\" to return to the test")
.style(Style::default().fg(Color::Red).bg(Color::Black));

frame.render_widget(Clear, frame_rect);
frame.render_widget(paragraph, frame_rect);

return;
}

// check if there is enough space horizontally to display the help message
if frame_rect.width - 2 <= longest_help_msg_len as u16 {
let paragraph = Paragraph::new(
"Terminal window is too narrow\nto display the help window\nresize the terminal\nor press the \"?\" key\nto return to the test",
)
.style(Style::default().fg(Color::Red).bg(Color::Black));

frame.render_widget(Clear, frame_rect);
frame.render_widget(paragraph, frame_rect);

return;
}

// Create a clear overlay to dim the background
frame.render_widget(
Paragraph::new("")
.style(Style::default().bg(Color::Black).fg(Color::DarkGray))
.block(Block::default()),
frame_rect,
);

let area = Self::get_centered_rect(
longest_help_msg_len.try_into().unwrap(),
help_text_lines_count.try_into().unwrap(),
frame.size(),
);

// Clear the background area first.
frame.render_widget(Clear, area);

let block = Block::default().title(" Help ").borders(Borders::ALL);

let inner_area = block.inner(area);

// Create constraints dynamically based on help_text length
let constraints = vec![Constraint::Length(1); help_text.len()];
let constraints = vec![Constraint::Length(1); help_text_lines_count];

let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints(constraints)
.split(inner_area);

// Render block
frame.render_widget(block, area);

// Render text paragraphs
for (i, &text) in help_text.iter().enumerate() {
let paragraph = Paragraph::new(text).style(Style::default().fg(Color::White));
let paragraph = Paragraph::new(text);
frame.render_widget(paragraph, chunks[i]);
}
}

fn get_centered_rect(window_width: u16, window_height: u16, r: Rect) -> Rect {
let x = r.x + (r.width.saturating_sub(window_width + 2)) / 2;
let y = if r.height > window_height + 4 { 3 } else { 0 };

Rect::new(x, y, window_width + 2, window_height + 1)
}
}

// TODO: add tests
100 changes: 37 additions & 63 deletions src/runner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ use crate::expected_input::ExpectedInputInterface;
use crate::help_window::HelpWindow;
use crate::helpers::split_by_char_index;
use crate::test_results::{Stats, TestResults};
use ratatui::widgets::Block;
use ratatui::{
backend::Backend,
layout::{Alignment, Constraint, Direction, Layout, Rect},
Expand Down Expand Up @@ -135,31 +134,36 @@ impl Runner {
if let Event::Key(key) = event::read().context("Unable to read event")? {
if key.kind == KeyEventKind::Press {
match self.input_mode {
InputMode::Normal => match key.code {
KeyCode::Char('s') => {
// Hide help window if it's shown.
self.show_help = false;

start_time = if self.is_started {
start_time + pause_time.elapsed()
} else {
Instant::now()
};
self.is_started = true;
self.input_mode = InputMode::Editing;
}
KeyCode::Char('q') => {
// todo return canceled test error and handle it in main
return Ok(TestResults::new(
Stats::default(),
self.config.clone(),
false,
));
}
KeyCode::Char('?') => {
self.show_help = !self.show_help;
}
_ => {}
InputMode::Normal => match self.show_help {
true => match key.code {
KeyCode::Char('?') => {
self.show_help = false;
}
_ => {}
},
false => match key.code {
KeyCode::Char('s') => {
start_time = if self.is_started {
start_time + pause_time.elapsed()
} else {
Instant::now()
};
self.is_started = true;
self.input_mode = InputMode::Editing;
}
KeyCode::Char('q') => {
// TODO: return canceled test error and handle it in main
return Ok(TestResults::new(
Stats::default(),
self.config.clone(),
false,
));
}
KeyCode::Char('?') => {
self.show_help = true;
}
_ => {}
},
},
InputMode::Editing => match key.code {
// Crossterm returns `ctrl+w` or ``ctrl+h` when `ctrl+backspace` is pressed
Expand Down Expand Up @@ -246,18 +250,7 @@ impl Runner {

// Then render help window on top if needed
if self.show_help {
// Create a clear overlay to dim the background
let full_area = frame.size();
frame.render_widget(
Paragraph::new("")
.style(Style::default().bg(Color::Black).fg(Color::White))
.block(Block::default()),
full_area,
);

// Render the help window in the center
let help_area = centered_rect(60, 60, frame.size());
self.help_window.render(frame, help_area);
self.help_window.render(frame)
}
}

Expand Down Expand Up @@ -343,12 +336,12 @@ impl Runner {
let help_message = match self.input_mode {
InputMode::Normal => {
if self.is_started {
"press 's' to unpause the test, 'q' to quit, '?' for help"
"press 's' to resume the test, 'q' to quit, '?' for help"
} else {
"press 's' to start the test, 'q' to quit, '?' for help"
}
}
InputMode::Editing => "press 'Esc' to pause the test",
InputMode::Editing => "press '<Esc>' to pause the test",
};
self.print_block_of_text(
frame,
Expand Down Expand Up @@ -654,8 +647,8 @@ mod test {
vec![
vec![
("30 seconds left", Color::Yellow),
(" ", Color::Reset),
("press 'Esc' to pause the test", Color::Yellow),
(" ", Color::Reset),
("press '<Esc>' to pause the test", Color::Yellow),
],
vec![
("foobar", Color::Green),
Expand All @@ -669,7 +662,8 @@ mod test {
);

test_runner(&mut runner, buffer, |frame, runner| {
runner.render(frame, 30);
let duration = 30;
runner.render(frame, duration);
});
}

Expand Down Expand Up @@ -790,23 +784,3 @@ mod test {
runner.move_cursor(&mut frame, area, input_current_line_len, current_line_index)
}
}

fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect {
let popup_layout = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Percentage((100 - percent_y) / 2),
Constraint::Percentage(percent_y),
Constraint::Percentage((100 - percent_y) / 2),
])
.split(r);

Layout::default()
.direction(Direction::Horizontal)
.constraints([
Constraint::Percentage((100 - percent_x) / 2),
Constraint::Percentage(percent_x),
Constraint::Percentage((100 - percent_x) / 2),
])
.split(popup_layout[1])[1]
}
Loading