use crate::graphics::menus::retro_parameter_table::generic_style::{
get_formated_footer, ScrollBarCustomRetroStyle, TableCustomRetroStyle, DISPLAY_CELL_OUT_SPACE,
};
use crate::graphics::menus::utils_layout::{
calculate_max_column_widths, calculate_sum_inner_row_heights, constraint_length_from_widths,
};
use crossterm::event;
use crossterm::event::{Event, KeyCode, KeyEventKind};
use ratatui::widgets::FrameExt;
use ratatui::{
layout::{Constraint, Layout}, widgets::Paragraph,
DefaultTerminal,
Frame,
};
use unicode_segmentation::UnicodeSegmentation;
pub trait ActionParameter {
fn apply_and_save(&mut self, rows: &[RowData], current_preset: Option<u16>);
}
#[derive(Clone)]
pub struct FooterData {
pub symbol: String,
pub text: String,
pub value: Option<u16>,
}
pub struct ActionInputs<'a> {
pub key: Vec<KeyCode>,
pub action: Vec<TableParameterAction<'a>>,
}
#[allow(clippy::type_complexity)]
pub enum TableParameterAction<'a> {
NextValue,
PreviousValue,
NextRow,
PreviousRow,
Quit,
ApplyAndSave(&'a mut dyn ActionParameter),
LoadPreset(
u16,
fn(u16) -> (Option<Vec<RowData>>, Option<Vec<FooterData>>),
),
}
#[derive(Clone)]
pub enum CellValue {
Text(String),
Options {
option_name: String,
values: Vec<String>,
index: usize,
index_ini: usize,
},
}
impl CellValue {
#[must_use]
pub fn new(text: String) -> Self {
Self::Text(text)
}
#[must_use]
pub fn new_with_options(option_name: String, values: Vec<String>, index: usize) -> Self {
Self::Options {
option_name,
values,
index,
index_ini: index,
}
}
fn next_value(&mut self) {
if let CellValue::Options { values, index, .. } = self {
*index = (*index + 1) % values.len();
}
}
fn previous_value(&mut self) {
if let CellValue::Options { values, index, .. } = self {
let max = values.len();
*index = (*index + max.saturating_sub(1)) % max;
}
}
fn width(&self) -> usize {
match self {
CellValue::Options { values, .. } => {
let max = values
.iter()
.map(|v| v.as_str().graphemes(true).count())
.max()
.unwrap_or(0);
max + DISPLAY_CELL_OUT_SPACE
}
CellValue::Text(v) => v.split('\n').map(|s| s.chars().count()).max().unwrap_or(0),
}
}
fn height(&self) -> usize {
match self {
CellValue::Options { values, .. } => values
.iter()
.map(|v| v.split('\n').count())
.max()
.unwrap_or(0),
CellValue::Text(v) => v.split('\n').count(),
}
}
}
#[derive(Clone)]
pub struct RowData {
pub cells: Vec<CellValue>,
}
impl RowData {
#[must_use]
pub fn new(cells: Vec<CellValue>) -> Self {
Self { cells }
}
pub(crate) fn get_cell_widths(&self) -> Vec<usize> {
self.cells.iter().map(CellValue::width).collect()
}
pub(crate) fn get_cell_heights(&self) -> Vec<usize> {
self.cells.iter().map(CellValue::height).collect()
}
fn next_cell_value(&mut self) {
for c in &mut self.cells {
c.next_value();
}
}
fn previous_cell_value(&mut self) {
for c in &mut self.cells {
c.previous_value();
}
}
}
pub struct GenericMenu<'a> {
table_custom: TableCustomRetroStyle<'a>,
scrollbar: ScrollBarCustomRetroStyle<'a>,
selected_row: usize,
info_footer: Paragraph<'a>,
info_footer_data: Vec<FooterData>,
vertical_layout: Layout,
current_preset: Option<u16>,
}
impl<'a> GenericMenu<'a> {
#[must_use]
pub fn new(
rows: Vec<RowData>,
headers: &[String],
info_footer: Vec<FooterData>,
current_preset: Option<u16>,
) -> Self {
let column_widths = calculate_max_column_widths(&rows, headers);
let constraints = constraint_length_from_widths(&column_widths);
let row_sum_height = calculate_sum_inner_row_heights(&rows);
let vertical_layout = Layout::vertical([
Constraint::Min(1),
Constraint::Length(
u16::try_from(headers.len()).expect("too much headers to store :p "),
),
]);
Self {
table_custom: TableCustomRetroStyle::new(headers, rows, 0, constraints),
scrollbar: ScrollBarCustomRetroStyle::new(row_sum_height),
selected_row: 0,
info_footer: get_formated_footer(&info_footer),
info_footer_data: info_footer,
vertical_layout,
current_preset,
}
}
pub fn next_row(&mut self) {
let i = match self.table_custom.state.selected() {
Some(i) => (i + 1) % self.table_custom.rows.len(),
None => 0,
};
self.table_custom.state.select(Some(i));
self.selected_row = i;
self.scrollbar.scroll_state =
self.scrollbar
.scroll_state
.position(calculate_sum_inner_row_heights(
&self.table_custom.rows[..i],
));
}
pub fn previous_row(&mut self) {
let i = match self.table_custom.state.selected() {
Some(i) => (i + self.table_custom.rows.len() - 1) % self.table_custom.rows.len(),
None => 0,
};
self.table_custom.state.select(Some(i));
self.selected_row = i;
self.scrollbar.scroll_state =
self.scrollbar
.scroll_state
.position(calculate_sum_inner_row_heights(
&self.table_custom.rows[..i],
));
}
pub fn next_parameter_value(&mut self) {
if let Some(row) = self.table_custom.rows.get_mut(self.selected_row) {
row.next_cell_value();
}
}
pub fn previous_parameter_value(&mut self) {
if let Some(row) = self.table_custom.rows.get_mut(self.selected_row) {
row.previous_cell_value();
}
}
pub fn run(
&mut self,
mut actions_inputs: Vec<ActionInputs<'a>>,
terminal: &mut DefaultTerminal,
) {
loop {
terminal.draw(|frame| self.draw(frame)).unwrap();
if let Event::Key(key) = event::read().unwrap() {
if key.kind == KeyEventKind::Press {
for action_input in &mut actions_inputs {
for key_code in action_input.key.clone() {
if key_code == key.code {
for unitary_tp_action in &mut action_input.action {
match unitary_tp_action {
TableParameterAction::NextValue => {
self.next_parameter_value();
}
TableParameterAction::PreviousValue => {
self.previous_parameter_value();
}
TableParameterAction::NextRow => {
self.next_row();
}
TableParameterAction::PreviousRow => {
self.previous_row();
}
TableParameterAction::ApplyAndSave(action) => {
action.apply_and_save(
&self.table_custom.rows,
self.current_preset,
);
}
TableParameterAction::Quit => {
return;
}
TableParameterAction::LoadPreset(index, loader) => {
let (new_rows, new_footer) = loader(*index);
let new_rows =
new_rows.unwrap_or(self.table_custom.rows.clone());
let new_footer =
new_footer.unwrap_or(self.info_footer_data.clone());
*self = Self::new(
new_rows,
&self.table_custom.headers,
new_footer,
Some(*index),
);
}
} } } } } } } } }
fn draw(&mut self, frame: &mut Frame) {
let rects = self.vertical_layout.split(frame.area());
self.table_custom
.update_table_color_background(self.selected_row);
self.table_custom.render(frame, rects[0]);
frame.render_stateful_widget(
self.scrollbar.widget.clone(),
rects[0].inner(self.scrollbar.margin),
&mut self.scrollbar.scroll_state,
);
frame.render_widget_ref(&self.info_footer, rects[1]);
}
}
#[must_use]
pub fn get_default_action_input<'a>() -> Vec<ActionInputs<'a>> {
vec![
ActionInputs {
key: vec![KeyCode::Down],
action: vec![TableParameterAction::NextRow],
},
ActionInputs {
key: vec![KeyCode::Up],
action: vec![TableParameterAction::PreviousRow],
},
ActionInputs {
key: vec![KeyCode::Right],
action: vec![TableParameterAction::NextValue],
},
ActionInputs {
key: vec![KeyCode::Left],
action: vec![TableParameterAction::PreviousValue],
},
ActionInputs {
key: vec![KeyCode::Esc],
action: vec![TableParameterAction::Quit],
},
]
}