use crossterm::event::{KeyCode, KeyEvent, MouseButton, MouseEventKind};
use ratatui::{
prelude::*,
style::{Color, Style},
widgets::*,
};
use crate::{app::Action, mouse_input::MouseInput};
use crate::menus::help::HelpContext;
use crate::menus::{centered_popup, wrap_index, Menu, OpenMenu, PopupSize};
use super::salloc_list::SallocList;
use super::{entry_menu::EntryMenu, salloc_entry::SallocEntry};
pub enum Focus {
List,
Entry,
}
pub struct SallocMenu {
open: bool,
rect: Rect,
preset_pane: Rect,
settings_pane: Rect,
salloc_list: SallocList<SallocEntry>,
entry_menu: EntryMenu,
state: ListState,
focus: Focus,
}
impl Default for SallocMenu {
fn default() -> Self {
Self::new()
}
}
impl SallocMenu {
pub fn new() -> SallocMenu {
let salloc_list = SallocList::load(None).unwrap_or_else(|_| SallocList::new());
let mut salloc_menu = SallocMenu {
open: false,
salloc_list,
rect: Rect::default(),
preset_pane: Rect::default(),
settings_pane: Rect::default(),
entry_menu: EntryMenu::new(None),
state: ListState::default(),
focus: Focus::List,
};
salloc_menu.set_index(0);
salloc_menu
}
}
impl SallocMenu {
pub fn activate(&mut self) {
self.open = true;
}
pub fn deactivate(&mut self) {
self.open = false;
let _ = self.salloc_list.save(None);
}
pub fn set_index(&mut self, index: i32) {
let new_index = wrap_index(index as isize, 0, self.salloc_list.len() + 1);
self.state.select(Some(new_index));
self.entry_menu = EntryMenu::new(self.get_salloc_entry());
}
fn next(&mut self) {
let index = self.state.selected();
if let Some(ind) = index {
self.set_index(ind as i32 + 1)
};
}
fn previous(&mut self) {
let index = self.state.selected();
if let Some(ind) = index {
self.set_index(ind as i32 - 1)
};
}
fn focus_preset(&mut self) {
self.entry_menu.is_active = false;
let _ = self.salloc_list.save(None);
self.focus = Focus::List
}
fn focus_settings(&mut self) {
self.entry_menu.is_active = true;
self.focus = Focus::Entry
}
fn toggle_focus(&mut self) {
match self.focus {
Focus::List => self.focus_settings(),
Focus::Entry => self.focus_preset(),
}
}
fn get_salloc_entry(&self) -> Option<&SallocEntry> {
let index = self.state.selected()?;
self.salloc_list.entries.get(index)
}
fn create_new_salloc_entry(&mut self) {
self.salloc_list.entries.push(SallocEntry::new());
}
fn set_entry(&mut self, entry: SallocEntry) {
let index = match self.state.selected() {
Some(ind) => ind,
None => return,
};
if index < self.salloc_list.len() {
self.salloc_list.entries[index] = entry;
} else {
self.salloc_list.entries.push(entry);
}
}
fn start_salloc(&mut self, action: &mut Action) {
match self.get_salloc_entry() {
Some(entry) => {
let cmd = entry.start();
*action = Action::StartSalloc(cmd);
self.deactivate();
}
None => {
self.create_new_salloc_entry();
}
}
}
pub fn delete_current_entry(&mut self) {
let index = match self.state.selected() {
Some(ind) => ind,
None => return,
};
if index == self.salloc_list.len() {
return;
}
self.salloc_list.entries.remove(index);
self.set_index(index.saturating_sub(1) as i32);
}
}
impl Menu for SallocMenu {
fn is_open(&self) -> bool {
self.open
}
fn render(&mut self, f: &mut Frame, _area: &Rect) {
let rect = centered_popup(f.area(), PopupSize::Fraction(0.8), PopupSize::Fraction(0.8));
self.rect = rect;
f.render_widget(Clear, rect);
let block = Block::default()
.title_top(Line::from(" SALLOC ").alignment(Alignment::Center))
.borders(Borders::ALL)
.border_style(Style::default().fg(Color::Blue))
.title_style(
Style::default()
.fg(Color::Blue)
.add_modifier(Modifier::BOLD),
);
f.render_widget(block.clone(), rect);
let layout = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(30), Constraint::Percentage(70)])
.split(block.inner(rect));
self.preset_pane = layout[0];
self.settings_pane = layout[1];
self.render_list(f, &layout[0]);
self.render_entry(f, &layout[1]);
}
fn input(&mut self, action: &mut Action, key_event: KeyEvent) -> bool {
if key_event.code == KeyCode::Tab {
self.toggle_focus();
return true;
}
match self.focus {
Focus::List => self.input_list(action, key_event),
Focus::Entry => self.input_entry(action, key_event),
}
}
fn mouse_input(&mut self, action: &mut Action, mouse_input: &mut MouseInput) {
if let Some(MouseEventKind::Down(MouseButton::Left)) = mouse_input.kind() {
if !self.rect.contains(mouse_input.get_position()) {
self.deactivate();
mouse_input.click();
return;
}
if self.preset_pane.contains(mouse_input.get_position()) {
self.focus_preset();
} else if self.settings_pane.contains(mouse_input.get_position()) {
self.focus_settings();
}
};
match self.focus {
Focus::List => self.mouse_input_list(action, mouse_input),
Focus::Entry => self.entry_menu.mouse_input(action, mouse_input),
}
mouse_input.handled = true;
}
}
impl SallocMenu {
fn render_list(&mut self, f: &mut Frame, area: &Rect) {
let mut items: Vec<ListItem> = self
.salloc_list
.entries
.iter()
.map(|entry| ListItem::new(entry.preset_name.clone()))
.collect();
items.push(ListItem::new("Create new".to_string()));
let highlight_style = match self.focus {
Focus::List => Style::default().fg(Color::Blue),
Focus::Entry => Style::default(),
};
let control_hint = match self.focus {
Focus::List => "",
Focus::Entry => "<tab>",
};
let list = List::new(items)
.block(
Block::default()
.title("Presets:")
.title_top(Line::from(control_hint).alignment(Alignment::Right))
.borders(Borders::ALL)
.border_style(highlight_style),
)
.highlight_style(Style::default().add_modifier(Modifier::BOLD))
.highlight_style(highlight_style.reversed());
f.render_stateful_widget(list, *area, &mut self.state);
}
fn render_entry(&mut self, f: &mut Frame, area: &Rect) {
let highlight_style = match self.focus {
Focus::List => Style::default(),
Focus::Entry => Style::default().fg(Color::Blue),
};
let control_hint = match self.focus {
Focus::List => "<tab>",
Focus::Entry => "",
};
let block = Block::default()
.title("Settings:")
.title_top(Line::from(control_hint).alignment(Alignment::Right))
.borders(Borders::ALL)
.border_style(highlight_style);
f.render_widget(block.clone(), *area);
self.entry_menu.render(f, &block.inner(*area));
}
}
impl SallocMenu {
fn input_list(&mut self, action: &mut Action, key_event: KeyEvent) -> bool {
match key_event.code {
KeyCode::Esc | KeyCode::Char('q') => {
self.deactivate();
}
KeyCode::Down | KeyCode::Char('j') => {
self.next();
}
KeyCode::Up | KeyCode::Char('k') => {
self.previous();
}
KeyCode::Enter | KeyCode::Char('l') => {
self.start_salloc(action);
}
KeyCode::Char('d')
if self.get_salloc_entry().is_some() => {
*action = Action::RemoveSallocEntryDialog;
}
KeyCode::Char('?') => {
*action = Action::OpenMenu(OpenMenu::Help(HelpContext::AllocationMenu));
}
_ => {}
}
true
}
fn input_entry(&mut self, action: &mut Action, key_event: KeyEvent) -> bool {
let status = self.entry_menu.input(action, key_event);
if status {
let new_entry = self.entry_menu.get_entry();
self.set_entry(new_entry);
return true;
}
match key_event.code {
KeyCode::Esc | KeyCode::Char('q') => {
self.deactivate();
return true;
}
KeyCode::Char('?') => {
*action = Action::OpenMenu(OpenMenu::Help(HelpContext::AllocationMenu));
return true;
}
_ => {}
}
true
}
}
impl SallocMenu {
fn mouse_input_list(&mut self, action: &mut Action, mouse_input: &mut MouseInput) {
if let Some(mouse_event_kind) = mouse_input.kind() {
match mouse_event_kind {
MouseEventKind::Down(MouseButton::Left) => {
let mouse_pos = mouse_input.get_position();
let mut rel_y = mouse_pos.y.saturating_sub(self.preset_pane.y);
rel_y = rel_y.saturating_sub(1);
let new_index = rel_y as usize + self.state.offset();
self.set_index(new_index as i32);
if mouse_input.is_double_click() {
self.start_salloc(action);
}
mouse_input.click();
}
MouseEventKind::ScrollUp => {
self.previous();
}
MouseEventKind::ScrollDown => {
self.next();
}
_ => {}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::menus::Menu;
use crossterm::event::KeyModifiers;
fn menu_with_entries(entries: Vec<SallocEntry>) -> SallocMenu {
let mut menu = SallocMenu::new();
menu.salloc_list = SallocList { entries };
menu.set_index(0);
menu
}
fn key(code: KeyCode) -> KeyEvent {
KeyEvent::new(code, KeyModifiers::NONE)
}
#[test]
fn test_input_on_create_new_row_does_not_panic() {
let mut menu = menu_with_entries(vec![]);
menu.activate();
assert_eq!(menu.state.selected(), Some(0));
assert!(menu.get_salloc_entry().is_none());
let mut action = Action::None;
menu.input(&mut action, key(KeyCode::Tab));
menu.input(&mut action, key(KeyCode::Down));
assert_eq!(menu.salloc_list.len(), 1);
assert_eq!(menu.state.selected(), Some(0));
}
#[test]
fn test_set_entry_on_create_new_row_pushes_entry() {
let mut menu = menu_with_entries(vec![SallocEntry::new()]);
menu.set_index(1);
let mut entry = SallocEntry::new();
entry.preset_name = "created".to_string();
menu.set_entry(entry);
assert_eq!(menu.salloc_list.len(), 2);
assert_eq!(menu.salloc_list.entries[1].preset_name, "created");
let mut entry = SallocEntry::new();
entry.preset_name = "updated".to_string();
menu.set_entry(entry);
assert_eq!(menu.salloc_list.len(), 2);
assert_eq!(menu.salloc_list.entries[1].preset_name, "updated");
}
#[test]
fn test_delete_first_entry_keeps_selection_on_first() {
let mut first = SallocEntry::new();
first.preset_name = "first".to_string();
let mut second = SallocEntry::new();
second.preset_name = "second".to_string();
let mut menu = menu_with_entries(vec![first, second]);
menu.delete_current_entry();
assert_eq!(menu.salloc_list.len(), 1);
assert_eq!(menu.state.selected(), Some(0));
assert_eq!(menu.get_salloc_entry().unwrap().preset_name, "second");
}
#[test]
fn test_delete_only_entry_selects_create_new() {
let mut menu = menu_with_entries(vec![SallocEntry::new()]);
menu.delete_current_entry();
assert_eq!(menu.salloc_list.len(), 0);
assert_eq!(menu.state.selected(), Some(0));
assert!(menu.get_salloc_entry().is_none());
}
}