use crate::tui::component::Action::Pass; use crate::tui::component::{Action, Component, ComponentState}; use crate::tui::title_bar::MenuBarItemOption::{ About, Exit, Reload, Save, ShowHideExplorer, ShowHideLogger, }; use ratatui::buffer::Buffer; use ratatui::crossterm::event::{KeyCode, KeyEvent}; use ratatui::layout::Rect; use ratatui::style::{Color, Modifier, Style}; use ratatui::text::Line; use ratatui::widgets::{ Block, Borders, Clear, List, ListItem, ListState, StatefulWidget, Tabs, Widget, }; use strum::{EnumIter, FromRepr, IntoEnumIterator}; #[derive(Debug, Clone, Copy, PartialEq, Eq, FromRepr, EnumIter)] enum MenuBarItem { File, View, Help, } #[derive(Debug, Clone, Copy, PartialEq, Eq, FromRepr, EnumIter)] enum MenuBarItemOption { Save, Reload, Exit, ShowHideExplorer, ShowHideLogger, About, } impl MenuBarItemOption { fn id(&self) -> &str { match self { MenuBarItemOption::Save => "Save", MenuBarItemOption::Reload => "Reload", MenuBarItemOption::Exit => "Exit", MenuBarItemOption::ShowHideExplorer => "Show / hide explorer", MenuBarItemOption::ShowHideLogger => "Show / hide logger", MenuBarItemOption::About => "About", } } } impl MenuBarItem { pub fn next(self) -> Self { let cur = self as usize; let next = cur.saturating_add(1); Self::from_repr(next).unwrap_or(self) } pub fn prev(self) -> Self { let cur = self as usize; let prev = cur.saturating_sub(1); Self::from_repr(prev).unwrap_or(self) } pub fn id(&self) -> &str { match self { MenuBarItem::File => "File", MenuBarItem::View => "View", MenuBarItem::Help => "Help", } } pub fn options(&self) -> &[MenuBarItemOption] { match self { MenuBarItem::File => &[Save, Reload, Exit], MenuBarItem::View => &[ShowHideExplorer, ShowHideLogger], MenuBarItem::Help => &[About], } } } pub struct MenuBar { selected: MenuBarItem, opened: Option, pub(crate) component_state: ComponentState, list_state: ListState, } impl MenuBar { const DEFAULT_HELP: &str = "(←/h)/(→/l): Select option | Enter: Choose selection"; pub fn new() -> Self { Self { selected: MenuBarItem::File, opened: None, component_state: ComponentState::default().with_help_text(Self::DEFAULT_HELP), list_state: ListState::default().with_selected(Some(0)), } } fn render_title_bar(&self, area: Rect, buf: &mut Buffer) { let titles: Vec = MenuBarItem::iter() .map(|item| Line::from(item.id().to_owned())) .collect(); let tabs_style = Style::default(); let highlight_style = if self.opened.is_some() { Style::default().bg(Color::Blue).fg(Color::White) } else { Style::default().bg(Color::Cyan).fg(Color::Black) }; Tabs::new(titles) .style(tabs_style) .block(Block::default().borders(Borders::ALL)) .highlight_style(highlight_style) .select(self.selected as usize) .render(area, buf); } fn render_drop_down( &mut self, title_bar_anchor: Rect, area: Rect, buf: &mut Buffer, opened: MenuBarItem, ) { let popup_area = Self::rect_under_option(title_bar_anchor, area, 40, 10); Clear::default().render(popup_area, buf); let options = opened.options().iter().map(|i| ListItem::new(i.id())); StatefulWidget::render( List::new(options) .block(Block::bordered().title(self.selected.id())) .highlight_style( Style::default() .bg(Color::Blue) .fg(Color::White) .add_modifier(Modifier::BOLD), ) .highlight_symbol(">> "), popup_area, buf, &mut self.list_state, ); } fn rect_under_option(anchor: Rect, area: Rect, width: u16, height: u16) -> Rect { // TODO: X offset for item option? It's fine as-is, but it might look nicer. Rect { x: anchor.x, y: anchor.y + anchor.height, width: width.min(area.width), height, } } pub fn render(&mut self, area: Rect, buf: &mut Buffer) where Self: Sized, { let title_bar_area = Rect { x: area.x, y: area.y, width: area.width, height: 3, }; self.render_title_bar(title_bar_area, buf); if let Some(opened) = self.opened { self.render_drop_down(title_bar_area, area, buf, opened); } } } impl Component for MenuBar { fn handle_key_events(&mut self, key: KeyEvent) -> anyhow::Result { if self.opened.is_some() { // Keybinds for popup menu. match key.code { KeyCode::Up | KeyCode::Char('k') => { self.list_state.select_previous(); Ok(Action::Handled) } KeyCode::Down | KeyCode::Char('j') => { self.list_state.select_next(); Ok(Action::Handled) } KeyCode::Enter => { if let Some(selected) = self.list_state.selected() { let seletion = self.selected.options()[selected]; return match seletion { Save => Ok(Action::Save), Exit => Ok(Action::Quit), Reload => Ok(Action::Noop), // TODO ShowHideExplorer => Ok(Action::Noop), // TODO ShowHideLogger => Ok(Action::Noop), // TODO About => Ok(Action::Noop), // TODO }; } Ok(Action::Noop) } KeyCode::Esc | KeyCode::Char('q') => { self.opened = None; self.component_state.help_text = Self::DEFAULT_HELP.to_string(); self.list_state.select_first(); Ok(Action::Handled) } _ => Ok(Action::Noop), } } else { // Keybinds for title bar. match key.code { KeyCode::Left | KeyCode::Char('h') => { self.selected = self.selected.prev(); Ok(Action::Handled) } KeyCode::Right | KeyCode::Char('l') => { self.selected = self.selected.next(); Ok(Action::Handled) } KeyCode::Enter => { self.opened = Some(self.selected); self.component_state.help_text = concat!( "(↑/k)/(↓/j): Select option | Enter: Choose selection |", " ESC/Q: Close drop-down menu" ) .to_string(); Ok(Action::Handled) } _ => Ok(Action::Noop), } } } }