From 66cd868fd96a77891abac2a06e28cf1263848786 Mon Sep 17 00:00:00 2001 From: radlinskii Date: Sun, 27 Oct 2024 19:28:33 +0100 Subject: [PATCH 1/2] improve help window ux --- src/help_window.rs | 77 ++++++++++++++++++++++++++++++----------- src/runner.rs | 86 +++++++++++++++++++--------------------------- 2 files changed, 91 insertions(+), 72 deletions(-) diff --git a/src/help_window.rs b/src/help_window.rs index b50a52c..29a7938 100644 --- a/src/help_window.rs +++ b/src/help_window.rs @@ -1,6 +1,6 @@ +use anyhow::{Context, Result}; use ratatui::{ layout::{Constraint, Direction, Layout, Rect}, - style::{Color, Style}, widgets::{Block, Borders, Clear, Paragraph}, }; @@ -13,32 +13,45 @@ impl HelpWindow { HelpWindow } - pub fn render(&self, frame: &mut impl FrameWrapperInterface, area: Rect) { + pub fn render(&self, frame: &mut impl FrameWrapperInterface) -> Result<()> { + let help_text = vec![ + "", + " Navigation:", + " 's' - Start/resume the test", + " - Pause the test", + " 'q' - Quit", + " '?' - Close this window", + "", + " Configuration:", + " --duration - 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() + .context("Unable to get the length of longest line from help window text")?; + let help_text_lines_count = help_text.len(); + + let area = Self::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) - .style(Style::default().fg(Color::White).bg(Color::Black)); + let block = Block::default().title(" Help ").borders(Borders::ALL); let inner_area = block.inner(area); - let help_text = vec![ - "Navigation:", - "s - Start/unpause the test", - "Esc - Pause the test", - "q - Quit", - "", - "Configuration:", - "--duration - Set test duration", - "--numbers - Include numbers in the test", - "--uppercase - Include uppercase letters", - "Run 'donkeytype help' for more options", - ]; - // 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) @@ -50,8 +63,30 @@ impl HelpWindow { // 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]); } + + Ok(()) + } + + fn centered_rect(window_width: u16, window_height: u16, r: Rect) -> Rect { + let popup_layout = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Length(0), + Constraint::Length(window_height + 2), + Constraint::Length(r.height - window_height - 2), + ]) + .split(r); + + Layout::default() + .direction(Direction::Horizontal) + .constraints([ + Constraint::Length((r.width - window_width - 2) / 2), + Constraint::Length(window_width + 2), + Constraint::Length((r.width - window_width - 2) / 2), + ]) + .split(popup_layout[1])[1] } } diff --git a/src/runner.rs b/src/runner.rs index 703cdba..e65900c 100644 --- a/src/runner.rs +++ b/src/runner.rs @@ -135,31 +135,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 @@ -250,14 +255,13 @@ impl Runner { let full_area = frame.size(); frame.render_widget( Paragraph::new("") - .style(Style::default().bg(Color::Black).fg(Color::White)) + .style(Style::default().bg(Color::Black).fg(Color::DarkGray)) .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); + let _ = self.help_window.render(frame); } } @@ -343,12 +347,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 '' to pause the test", }; self.print_block_of_text( frame, @@ -655,7 +659,7 @@ mod test { vec![ ("30 seconds left", Color::Yellow), (" ", Color::Reset), - ("press 'Esc' to pause the test", Color::Yellow), + ("press '' to pause the test", Color::Yellow), ], vec![ ("foobar", Color::Green), @@ -790,23 +794,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] -} From 2ded2565580536454500f901fd95b67db251ccb3 Mon Sep 17 00:00:00 2001 From: radlinskii Date: Tue, 5 Nov 2024 16:11:32 +0100 Subject: [PATCH 2/2] add size constraints to rendering help window --- src/help_window.rs | 87 +++++++++++++++++++++++++++++----------------- src/runner.rs | 20 +++-------- 2 files changed, 60 insertions(+), 47 deletions(-) diff --git a/src/help_window.rs b/src/help_window.rs index 29a7938..67c4c60 100644 --- a/src/help_window.rs +++ b/src/help_window.rs @@ -1,6 +1,6 @@ -use anyhow::{Context, Result}; use ratatui::{ layout::{Constraint, Direction, Layout, Rect}, + style::{Color, Style}, widgets::{Block, Borders, Clear, Paragraph}, }; @@ -13,14 +13,21 @@ impl HelpWindow { HelpWindow } - pub fn render(&self, frame: &mut impl FrameWrapperInterface) -> Result<()> { + pub fn render(&self, frame: &mut impl FrameWrapperInterface) { + let frame_rect = frame.size(); + + if frame_rect.height < 3 { + frame.render_widget(Clear, frame_rect); + return; + } + let help_text = vec![ "", " Navigation:", - " 's' - Start/resume the test", + " 's' - Start/resume the test", " - Pause the test", - " 'q' - Quit", - " '?' - Close this window", + " 'q' - Quit", + " '?' - Toggle this window", "", " Configuration:", " --duration - Set test duration", @@ -31,18 +38,48 @@ impl HelpWindow { "", ]; - let longest_help_msg_len = help_text - .iter() - .map(|s| s.len()) - .max() - .context("Unable to get the length of longest line from help window text")?; + let longest_help_msg_len = help_text.iter().map(|s| s.len()).max().unwrap(); let help_text_lines_count = help_text.len(); - let area = Self::centered_rect( + // 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); @@ -58,7 +95,6 @@ impl HelpWindow { .constraints(constraints) .split(inner_area); - // Render block frame.render_widget(block, area); // Render text paragraphs @@ -66,27 +102,14 @@ impl HelpWindow { let paragraph = Paragraph::new(text); frame.render_widget(paragraph, chunks[i]); } - - Ok(()) } - fn centered_rect(window_width: u16, window_height: u16, r: Rect) -> Rect { - let popup_layout = Layout::default() - .direction(Direction::Vertical) - .constraints([ - Constraint::Length(0), - Constraint::Length(window_height + 2), - Constraint::Length(r.height - window_height - 2), - ]) - .split(r); - - Layout::default() - .direction(Direction::Horizontal) - .constraints([ - Constraint::Length((r.width - window_width - 2) / 2), - Constraint::Length(window_width + 2), - Constraint::Length((r.width - window_width - 2) / 2), - ]) - .split(popup_layout[1])[1] + 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 diff --git a/src/runner.rs b/src/runner.rs index e65900c..3325300 100644 --- a/src/runner.rs +++ b/src/runner.rs @@ -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}, @@ -153,7 +152,7 @@ impl Runner { self.input_mode = InputMode::Editing; } KeyCode::Char('q') => { - // todo return canceled test error and handle it in main + // TODO: return canceled test error and handle it in main return Ok(TestResults::new( Stats::default(), self.config.clone(), @@ -251,17 +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::DarkGray)) - .block(Block::default()), - full_area, - ); - - // Render the help window in the center - let _ = self.help_window.render(frame); + self.help_window.render(frame) } } @@ -658,7 +647,7 @@ mod test { vec![ vec![ ("30 seconds left", Color::Yellow), - (" ", Color::Reset), + (" ", Color::Reset), ("press '' to pause the test", Color::Yellow), ], vec![ @@ -673,7 +662,8 @@ mod test { ); test_runner(&mut runner, buffer, |frame, runner| { - runner.render(frame, 30); + let duration = 30; + runner.render(frame, duration); }); }