diff options
| author | Mo <76752051+mo8it@users.noreply.github.com> | 2024-04-14 17:13:32 +0200 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2024-04-14 17:13:32 +0200 |
| commit | dc02c38a945fcafacf6d2d35f5d3e317e7185cb0 (patch) | |
| tree | bd3ad843a575650881b220c4b008fc7509917d24 /src/list/state.rs | |
| parent | 8c8f30d8ce3b732de649938d8945496bd769ac22 (diff) | |
| parent | 7526c6b1f92626df6ab8b4853535b73711bfada4 (diff) | |
Merge pull request #1942 from rust-lang/tui
TUI
Diffstat (limited to 'src/list/state.rs')
| -rw-r--r-- | src/list/state.rs | 261 |
1 files changed, 261 insertions, 0 deletions
diff --git a/src/list/state.rs b/src/list/state.rs new file mode 100644 index 0000000..2a1fef1 --- /dev/null +++ b/src/list/state.rs @@ -0,0 +1,261 @@ +use anyhow::{Context, Result}; +use ratatui::{ + layout::{Constraint, Rect}, + style::{Style, Stylize}, + text::Span, + widgets::{Block, Borders, HighlightSpacing, Paragraph, Row, Table, TableState}, + Frame, +}; +use std::fmt::Write; + +use crate::{app_state::AppState, progress_bar::progress_bar_ratatui}; + +#[derive(Copy, Clone, PartialEq, Eq)] +pub enum Filter { + Done, + Pending, + None, +} + +pub struct UiState<'a> { + pub table: Table<'static>, + pub message: String, + pub filter: Filter, + app_state: &'a mut AppState, + table_state: TableState, + n_rows: usize, +} + +impl<'a> UiState<'a> { + pub fn with_updated_rows(mut self) -> Self { + let current_exercise_ind = self.app_state.current_exercise_ind(); + + self.n_rows = 0; + let rows = self + .app_state + .exercises() + .iter() + .enumerate() + .filter_map(|(ind, exercise)| { + let exercise_state = if exercise.done { + if self.filter == Filter::Pending { + return None; + } + + "DONE".green() + } else { + if self.filter == Filter::Done { + return None; + } + + "PENDING".yellow() + }; + + self.n_rows += 1; + + let next = if ind == current_exercise_ind { + ">>>>".bold().red() + } else { + Span::default() + }; + + Some(Row::new([ + next, + exercise_state, + Span::raw(exercise.name), + Span::raw(exercise.path), + ])) + }); + + self.table = self.table.rows(rows); + + if self.n_rows == 0 { + self.table_state.select(None); + } else { + self.table_state.select(Some( + self.table_state + .selected() + .map_or(0, |selected| selected.min(self.n_rows - 1)), + )); + } + + self + } + + pub fn new(app_state: &'a mut AppState) -> Self { + let header = Row::new(["Next", "State", "Name", "Path"]); + + let max_name_len = app_state + .exercises() + .iter() + .map(|exercise| exercise.name.len()) + .max() + .unwrap_or(4) as u16; + + let widths = [ + Constraint::Length(4), + Constraint::Length(7), + Constraint::Length(max_name_len), + Constraint::Fill(1), + ]; + + let table = Table::default() + .widths(widths) + .header(header) + .column_spacing(2) + .highlight_spacing(HighlightSpacing::Always) + .highlight_style(Style::new().bg(ratatui::style::Color::Rgb(50, 50, 50))) + .highlight_symbol("🦀") + .block(Block::default().borders(Borders::BOTTOM)); + + let selected = app_state.current_exercise_ind(); + let table_state = TableState::default() + .with_offset(selected.saturating_sub(10)) + .with_selected(Some(selected)); + + let filter = Filter::None; + let n_rows = app_state.exercises().len(); + + let slf = Self { + table, + message: String::with_capacity(128), + filter, + app_state, + table_state, + n_rows, + }; + + slf.with_updated_rows() + } + + pub fn select_next(&mut self) { + if self.n_rows > 0 { + let next = self + .table_state + .selected() + .map_or(0, |selected| (selected + 1).min(self.n_rows - 1)); + self.table_state.select(Some(next)); + } + } + + pub fn select_previous(&mut self) { + if self.n_rows > 0 { + let previous = self + .table_state + .selected() + .map_or(0, |selected| selected.saturating_sub(1)); + self.table_state.select(Some(previous)); + } + } + + #[inline] + pub fn select_first(&mut self) { + if self.n_rows > 0 { + self.table_state.select(Some(0)); + } + } + + #[inline] + pub fn select_last(&mut self) { + if self.n_rows > 0 { + self.table_state.select(Some(self.n_rows - 1)); + } + } + + pub fn draw(&mut self, frame: &mut Frame) -> Result<()> { + let area = frame.size(); + + frame.render_stateful_widget( + &self.table, + Rect { + x: 0, + y: 0, + width: area.width, + height: area.height - 3, + }, + &mut self.table_state, + ); + + frame.render_widget( + Paragraph::new(progress_bar_ratatui( + self.app_state.n_done(), + self.app_state.exercises().len() as u16, + area.width, + )?) + .block(Block::default().borders(Borders::BOTTOM)), + Rect { + x: 0, + y: area.height - 3, + width: area.width, + height: 2, + }, + ); + + let message = if self.message.is_empty() { + // Help footer. + Span::raw( + "↓/j ↑/k home/g end/G │ filter <d>one/<p>ending │ <r>eset │ <c>ontinue at │ <q>uit", + ) + } else { + self.message.as_str().light_blue() + }; + frame.render_widget( + message, + Rect { + x: 0, + y: area.height - 1, + width: area.width, + height: 1, + }, + ); + + Ok(()) + } + + pub fn with_reset_selected(mut self) -> Result<Self> { + let Some(selected) = self.table_state.selected() else { + return Ok(self); + }; + + let (ind, exercise) = self + .app_state + .exercises() + .iter() + .enumerate() + .filter_map(|(ind, exercise)| match self.filter { + Filter::Done => exercise.done.then_some((ind, exercise)), + Filter::Pending => (!exercise.done).then_some((ind, exercise)), + Filter::None => Some((ind, exercise)), + }) + .nth(selected) + .context("Invalid selection index")?; + + exercise.reset()?; + self.message + .write_fmt(format_args!("The exercise {exercise} has been reset!"))?; + self.app_state.set_pending(ind)?; + + Ok(self.with_updated_rows()) + } + + pub fn selected_to_current_exercise(&mut self) -> Result<()> { + let Some(selected) = self.table_state.selected() else { + return Ok(()); + }; + + let ind = self + .app_state + .exercises() + .iter() + .enumerate() + .filter_map(|(ind, exercise)| match self.filter { + Filter::Done => exercise.done.then_some(ind), + Filter::Pending => (!exercise.done).then_some(ind), + Filter::None => Some(ind), + }) + .nth(selected) + .context("Invalid selection index")?; + + self.app_state.set_current_exercise_ind(ind) + } +} |
