192 lines
6.8 KiB
Rust
192 lines
6.8 KiB
Rust
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<Option<Action>> {
|
|
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<Option<Action>> {
|
|
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<Option<Action>> {
|
|
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<Option<Action>> {
|
|
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::<Row>()
|
|
.style(Style::default().fg(tailwind::BLUE.c200).bg(color))
|
|
.height(ITEM_HEIGHT as u16))
|
|
})
|
|
.collect::<Vec<_>>(),
|
|
self.current_mode.get_help_constraints()
|
|
)
|
|
.header(self.current_mode
|
|
.get_help_headers()
|
|
.into_iter()
|
|
.map(|h| Cell::from(Text::from(*h)))
|
|
.collect::<Row>()
|
|
.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(())
|
|
}
|
|
}
|