use color_eyre::Result; use crossterm::event::{KeyEvent, KeyCode}; use ratatui::{ layout::{Alignment, Constraint, Flex, Layout, Margin, Rect}, style::{palette::tailwind, Style, Modifier}, text::Text, widgets::{ Block, BorderType, Cell, Clear, HighlightSpacing, Paragraph, Row, Scrollbar, ScrollbarOrientation, ScrollbarState, Table, TableState, }, Frame }; use super::palette::StylePalette; use super::Component; use crate::{action::Action, app::Mode, config::Config}; #[derive(Debug, Clone)] pub struct Help { is_active: bool, current_mode: Mode, palette: StylePalette, scroll_state: ScrollbarState, table_state: TableState, } const ITEM_HEIGHT: usize = 3; impl Help { fn move_down(&mut self) -> Result> { let i = match self.table_state.selected() { Some(i) => { if i >= self.current_mode.get_help_data().len() { 0 } else { i + 1 } }, None => 0, }; self.table_state.select(Some(i)); self.scroll_state = self.scroll_state.position(i * ITEM_HEIGHT); Ok(None) } fn move_up(&mut self) -> Result> { let i = match self.table_state.selected() { Some(i) => i.saturating_sub(1), None => 0, }; self.table_state.select(Some(i)); self.scroll_state = self.scroll_state.position(i * ITEM_HEIGHT); Ok(None) } } impl Default for Help { fn default() -> Self { Self { is_active: false, current_mode: Mode::Menu, palette: Default::default(), scroll_state: ScrollbarState::new((Mode::Menu.get_help_data().len() - 1) * ITEM_HEIGHT), table_state: TableState::default(), } } } impl Component for Help { fn register_config_handler(&mut self, config: Config) -> Result<()> { if let Some(style) = config.styles.get(&crate::app::Mode::Help) { self.palette.with_normal_style(style.get("normal_style").copied()); self.palette.with_normal_border_style(style.get("normal_border_style").copied()); self.palette.with_normal_title_style(style.get("normal_title_style").copied()); self.palette.with_popup_style(style.get("popup_style").copied()); self.palette.with_popup_title_style(style.get("popup_title_style").copied()); } Ok(()) } fn update(&mut self, action: Action) -> Result> { match action { Action::Help if !self.is_active => self.is_active = true, Action::SetActiveScreen(mode) => { if self.current_mode != mode { self.current_mode = mode; self.table_state.select(None); self.scroll_state = ScrollbarState::new((self.current_mode.get_help_data().len() - 1) * ITEM_HEIGHT); } } _ => {} }; Ok(None) } fn handle_key_event(&mut self, key: KeyEvent) -> Result> { match key.code { KeyCode::Up | KeyCode::Char('k') => self.move_up(), KeyCode::Down | KeyCode::Char('j') => self.move_down(), KeyCode::Esc if self.is_active => { self.is_active = false; Ok(Some(Action::SetActiveScreen(self.current_mode))) }, _ => Ok(None), } } fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> { if self.is_active { let highlight_symbol = Text::from(vec![ "".into(), " █ ".into(), "".into(), ]); let table = Table::new( self.current_mode .get_help_data() .into_iter() .enumerate() .map(|(i, content)| { let color = match i % 2 { 0 => tailwind::SLATE.c950, _ => tailwind::SLATE.c900, }; Row::from(content .into_iter() .map(|data| Cell::from(Text::from(*data))) .collect::() .style(Style::default().fg(tailwind::BLUE.c200).bg(color)) .height(ITEM_HEIGHT as u16)) }) .collect::>(), self.current_mode.get_help_constraints() ) .header(self.current_mode .get_help_headers() .into_iter() .map(|h| Cell::from(Text::from(*h))) .collect::() .style(Style::default() .fg(tailwind::SLATE.c200) .bg(tailwind::BLUE.c900) ) .height(1) ) .highlight_style(Style::default() .add_modifier(Modifier::REVERSED) .fg(tailwind::BLUE.c400) .bg(tailwind::YELLOW.c200)) .highlight_spacing(HighlightSpacing::Always) .highlight_symbol(highlight_symbol) .block(Block::default() .style(Style::default().bg(tailwind::SLATE.c950)) .title_alignment(Alignment::Right) .title_style(tailwind::YELLOW.c200) .title(self.current_mode.get_help_title())); let scrollbar = Scrollbar::default() .orientation(ScrollbarOrientation::VerticalRight) .begin_symbol(None) .end_symbol(None); let footer = Paragraph::new(Text::from(self.current_mode.get_help_text())) .style(Style::default().fg(tailwind::SLATE.c200).bg(tailwind::SLATE.c950)) .centered() .block(Block::bordered() .border_type(BorderType::Double) .border_style(Style::default().fg(tailwind::BLUE.c400)) ); let v = Layout::vertical([Constraint::Length(23)]).flex(Flex::Center); let h = Layout::horizontal([Constraint::Length(65)]).flex(Flex::Center); let [area] = v.areas(area); let [area] = h.areas(area); let [main_area, footer_area] = Layout::vertical([ Constraint::Fill(1), Constraint::Length(3), ]).areas(area); frame.render_widget(Clear, area); frame.render_stateful_widget(table, main_area, &mut self.table_state); frame.render_stateful_widget(scrollbar, main_area.inner(Margin::new(1, 2)), &mut self.scroll_state); frame.render_widget(footer, footer_area); } Ok(()) } }