use crossterm::event::{KeyCode, KeyEvent};
use ratatui::layout::{Constraint, Flex, Layout};
use ratatui::prelude::Frame;
use ratatui::style::Stylize as _;
use ratatui::text::{Line, Span, Text};
use ratatui::widgets::{Block, BorderType, Borders};
use crate::ui::modal::{KeyboardHandler, ModalResult, UiModal};
use crate::ui::state::InfoPaneState;
use crate::ui::widget::{BgClear, InfoPane, Tab, TabStyle, tabs_line};
use crate::ui::{Theme, is_portrait};
#[derive(Debug, PartialEq, Eq)]
enum HelpTab {
Keys,
About,
}
#[derive(Debug)]
pub(crate) struct HelpModal<'a> {
help_state: InfoPaneState<'a>,
about_state: InfoPaneState<'a>,
help_tab: HelpTab,
}
impl UiModal for HelpModal<'static> {
fn render(&mut self, frame: &mut Frame, theme: &Theme) {
let area = frame.area();
let portrait = is_portrait(area);
let [modal_area] = Layout::vertical([Constraint::Length(21)])
.horizontal_margin(if portrait { 1 } else { area.width / 4 })
.flex(Flex::Center)
.areas(area);
let keys_tab_active = self.help_tab == HelpTab::Keys;
let tabs = [
Tab {
label: "Keys",
active: keys_tab_active,
},
Tab {
label: "About",
active: !keys_tab_active,
},
];
let tabs_style = TabStyle::from_theme(theme, true);
let block = Block::default()
.title_top(tabs_line(&tabs, tabs_style).left_aligned())
.borders(Borders::all())
.border_type(BorderType::Thick)
.border_style(theme.border_modal_style());
frame.render_widget(BgClear::new(theme.background_color()), modal_area);
frame.render_stateful_widget(
InfoPane::new(block),
modal_area,
if keys_tab_active {
&mut self.help_state
} else {
&mut self.about_state
},
);
}
}
impl KeyboardHandler for HelpModal<'_> {
fn handle_key(&mut self, key: KeyEvent) -> ModalResult {
match (&self.help_tab, key.code) {
(_, KeyCode::Char('q') | KeyCode::Char('?') | KeyCode::F(1) | KeyCode::Esc) => {
return ModalResult::Close;
}
(HelpTab::Keys, KeyCode::Tab | KeyCode::BackTab) => self.help_tab = HelpTab::About,
(HelpTab::Keys, KeyCode::Down | KeyCode::Char('j')) => self.help_state.scroll_down(),
(HelpTab::Keys, KeyCode::Up | KeyCode::Char('k')) => self.help_state.scroll_up(),
(HelpTab::About, KeyCode::Tab | KeyCode::BackTab) => self.help_tab = HelpTab::Keys,
(HelpTab::About, KeyCode::Down | KeyCode::Char('j')) => self.about_state.scroll_down(),
(HelpTab::About, KeyCode::Up | KeyCode::Char('k')) => self.about_state.scroll_up(),
_ => {}
}
ModalResult::None
}
}
impl<'a> HelpModal<'static> {
pub(crate) fn new(theme: &'a Theme) -> HelpModal<'static> {
Self {
help_state: InfoPaneState::new(HelpBuilder::new(theme).build()),
about_state: InfoPaneState::new(AboutBuilder::new(theme).build()),
help_tab: HelpTab::Keys,
}
}
}
struct HelpBuilder<'a> {
theme: &'a Theme,
}
impl<'a> HelpBuilder<'a> {
fn new(theme: &'a Theme) -> Self {
Self { theme }
}
fn build(&self) -> Text<'static> {
let mut lines = Vec::new();
lines.push(Line::from(" Key Action".fg(self.theme.pane_title_color())).bold());
lines.extend(self.navigation_lines());
lines.extend(self.render_lines());
lines.extend(self.modals_lines());
lines.extend(self.clipboard_lines());
Text::from(lines)
}
fn navigation_lines(&self) -> Vec<Line<'static>> {
let mut lines = Vec::new();
lines.push(
Line::from(" Navigation:")
.fg(self.theme.pane_title_color())
.bold(),
);
lines.extend(self.lines(
&["↑", "k"],
"Scroll up in the Info, Parameter or Output pane",
));
lines.extend(self.lines(
&["↓", "j"],
"Scroll down in the Info, Parameter or Output pane",
));
lines.extend(self.lines(&["Tab"], "Focus next pane/tab"));
lines.extend(self.lines(&["Shift+Tab"], "Focus previous pane/tab"));
lines.extend(self.lines(&["i"], "Focus Info pane"));
lines.extend(self.lines(
&["←", "h"],
"Switch the previous quick option in the Parameter pane",
));
lines.extend(self.lines(
&["→", "l"],
"Select the next quick option in the Parameter pane",
));
lines.extend(self.lines(&["Enter"], "Open parameter options"));
lines.extend(self.lines(&["Esc", "q", "Ctrl+c"], "Quit the application"));
lines.extend(self.lines(&["o"], "Focus Output pane"));
lines.extend(self.lines(&["?", "F1"], "Toggle help"));
lines
}
fn render_lines(&self) -> Vec<Line<'static>> {
let mut lines = Vec::new();
lines.push(Line::default());
lines.push(
Line::from(" Render:")
.fg(self.theme.pane_title_color())
.bold(),
);
lines.extend(self.lines(&["s"], "Open 'Render As' modal"));
lines.extend(self.lines(&["Ctrl+s"], "Quick render"));
lines.extend(self.lines(
&["Esc", "q", "Ctrl+c"],
"Stop rendering if it's in progress",
));
lines
}
fn modals_lines(&self) -> Vec<Line<'static>> {
let mut lines = Vec::new();
lines.push(Line::default());
lines.push(
Line::from(" Modals:")
.fg(self.theme.pane_title_color())
.bold(),
);
lines.extend(self.lines(&["Tab"], "Focus next field"));
lines.extend(self.lines(&["Shift+Tab"], "Focus previous field"));
lines.extend(self.lines(&["Space"], "Toggle a checkbox"));
lines.extend(self.lines(&["Esc"], "Close an active modal"));
lines
}
fn clipboard_lines(&self) -> Vec<Line<'static>> {
let mut lines = Vec::new();
lines.push(Line::default());
lines.push(
Line::from(" Clipboard:")
.fg(self.theme.pane_title_color())
.bold(),
);
lines.extend(self.lines(&["y"], "Open 'Copy to clipboard' modal"));
lines.extend(self.lines(&["yy"], "Copy a FFmpeg command to clipboard"));
lines.extend(self.lines(&["yp"], "Copy a preset to clipboard"));
lines.extend(self.lines(&["yi"], "Copy info to clipboard"));
lines.extend(self.lines(&["yo"], "Copy output to clipboard"));
lines
}
fn lines(&self, keys: &'static [&str], v: &'static str) -> Vec<Line<'static>> {
let key_style = self.theme.key_style();
let text_style = self.theme.text_color();
let repeated_style = self.theme.text_muted_color();
keys.iter()
.enumerate()
.map(|(i, k)| {
Line::from(vec![
Span::styled(format!("{k: >12} "), key_style),
Span::styled(
v.to_owned(),
if i == 0 { text_style } else { repeated_style },
),
])
})
.collect()
}
}
struct AboutBuilder<'a> {
theme: &'a Theme,
}
impl<'a> AboutBuilder<'a> {
fn new(theme: &'a Theme) -> Self {
Self { theme }
}
fn build(&self) -> Text<'static> {
let mut lines = Vec::new();
lines.extend(self.logo());
lines.extend(self.links());
lines.extend(self.ratatui_logo());
Text::from(lines)
}
fn logo(&self) -> Vec<Line<'static>> {
let color = self.theme.logo_color();
let mut lines: Vec<Line> = vec![];
let logo = [
"",
" /████████ /████████ ",
" | ██_____/| ██_____/ ",
" /██████ | ██ | ██ /██ /██",
" /██__ ██| █████ | █████| ██ | ██",
"| ████████| ██__/ | ██__/| ██ | ██",
"| ██_____/| ██ | ██ | ██ | ██",
"| ███████| ██ | ██ | ███████",
r" \_______/|__/ |__/ \____ ██",
" /██ | ██",
];
for line in logo {
lines.push(Line::from(line).fg(color).centered());
}
lines.push(
Line::from(format!(
" v{} | ██████/",
env!("CARGO_PKG_VERSION")
))
.fg(color)
.centered(),
);
lines.push(
Line::from(r" \______/ ")
.fg(color)
.centered(),
);
lines
}
fn ratatui_logo(&self) -> Vec<Line<'static>> {
let mut lines: Vec<Line> = vec![Line::default()];
let logo = [
"Built with Rust. Powered by".fg(self.theme.about_caption_color()),
"█▀▀▄ ▄▀▀▄▝▜▛▘▄▀▀▄▝▜▛▘█ █ █".fg(self.theme.ratatui_logo_color()),
"█▀▀▄ █▀▀█ ▐▌ █▀▀█ ▐▌ ▀▄▄▀ █".fg(self.theme.ratatui_logo_color()),
];
for line in logo {
lines.push(Line::from(line).centered());
}
lines
}
fn links(&self) -> Vec<Line<'static>> {
let link = self.theme.link_style();
vec![
Line::default(),
Line::from(Span::styled("https://github.com/aNNiMON/effy", link)).centered(),
]
}
}