use ratatui::Frame;
use ratatui::layout::Rect;
use ratatui::style::{Color, Modifier, Style};
use ratatui::text::{Line, Span};
use ratatui::widgets::Paragraph;
use crate::app::{Action, App};
use crate::keybindings::{InputContext, TuiAction};
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
pub enum TopMode {
#[default]
Board,
Triage,
Cycle,
NewIssue,
Config,
}
impl TopMode {
pub const ALL: [Self; 5] = [
Self::Board,
Self::Triage,
Self::Cycle,
Self::NewIssue,
Self::Config,
];
pub const fn name(self) -> &'static str {
match self {
Self::Board => "Board",
Self::Triage => "Triage",
Self::Cycle => "Cycle",
Self::NewIssue => "New Issue",
Self::Config => "Config",
}
}
pub const fn action(self) -> TuiAction {
match self {
Self::Board => TuiAction::SwitchToBoard,
Self::Triage => TuiAction::SwitchToTriage,
Self::Cycle => TuiAction::SwitchToCycle,
Self::NewIssue => TuiAction::SwitchToNewIssue,
Self::Config => TuiAction::SwitchToConfig,
}
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum NavDisplay {
Off,
Small,
#[default]
Default,
}
impl NavDisplay {
pub const fn next(self) -> Self {
match self {
Self::Off => Self::Small,
Self::Small => Self::Default,
Self::Default => Self::Off,
}
}
pub const fn width(self) -> u16 {
match self {
Self::Off => 0,
Self::Small => 4,
Self::Default => 14,
}
}
}
pub fn render(app: &App, f: &mut Frame, area: Rect) {
if app.nav_display == NavDisplay::Off || area.width == 0 {
return;
}
let mut lines: Vec<Line<'static>> = Vec::with_capacity(TopMode::ALL.len());
for mode in TopMode::ALL {
let chord = app.bindings.label(InputContext::Shell, mode.action());
let text = match app.nav_display {
NavDisplay::Small => chord,
_ => format!("{chord} {}", mode.name()),
};
let style = if mode == app.top_mode {
Style::default()
.fg(Color::Black)
.bg(Color::Cyan)
.add_modifier(Modifier::BOLD)
} else if app.is_hovered(&Action::SwitchMode(mode)) {
Style::default()
.fg(Color::White)
.bg(Color::DarkGray)
.add_modifier(Modifier::BOLD)
} else {
Style::default().fg(Color::Gray)
};
lines.push(Line::from(Span::styled(text, style)));
}
f.render_widget(Paragraph::new(lines), area);
let rows = area.height.min(TopMode::ALL.len() as u16);
for (index, mode) in TopMode::ALL.iter().enumerate() {
if index as u16 >= rows {
break;
}
let rect = Rect::new(area.x, area.y + index as u16, area.width, 1);
app.record_region(rect, Action::SwitchMode(*mode));
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn display_states_cycle_and_shrink() {
assert_eq!(NavDisplay::Off.next(), NavDisplay::Small);
assert_eq!(NavDisplay::Small.next(), NavDisplay::Default);
assert_eq!(NavDisplay::Default.next(), NavDisplay::Off);
assert_eq!(NavDisplay::Off.width(), 0);
assert!(NavDisplay::Small.width() < NavDisplay::Default.width());
assert_eq!(NavDisplay::Default, NavDisplay::default());
}
#[test]
fn every_mode_maps_to_a_distinct_shell_action() {
let actions: Vec<TuiAction> = TopMode::ALL.iter().map(|mode| mode.action()).collect();
for (index, action) in actions.iter().enumerate() {
assert!(
!actions[index + 1..].contains(action),
"two modes share the switch action {action:?}"
);
}
}
}