1
0
mirror of https://github.com/jugeeya/UltimateTrainingModpack.git synced 2026-01-22 10:20:24 +00:00

UI Code Refactor; Notifications; Save Defaults for Quick Menu (#461)

* Initial refactor

* Full refactor

* Depend only on pane creator flags

* Small refactor

* Small refactors; notification support

* Don't push event for every quick menu change

* Backend for defaults almost done

* Run tests on CI

* Finish save + reset defaults without confirmation

* Added slider menu UI

---------

Co-authored-by: xhudaman <edell.matthew@gmail.com>
This commit is contained in:
jugeeya
2023-02-02 15:01:11 -08:00
committed by GitHub
parent f014acfb5c
commit d5c0d636a0
15 changed files with 2077 additions and 1535 deletions

View File

@@ -1,4 +1,4 @@
use training_mod_consts::{Slider, SubMenu, SubMenuType, Toggle, UiMenu};
use training_mod_consts::{MenuJsonStruct, Slider, SubMenu, SubMenuType, Toggle, UiMenu, ui_menu, TrainingModpackMenu};
use tui::{
backend::Backend,
layout::{Constraint, Corner, Direction, Layout, Rect},
@@ -20,6 +20,14 @@ use crate::list::{MultiStatefulList, StatefulList};
static NX_TUI_WIDTH: u16 = 66;
#[derive(PartialEq)]
pub enum AppPage {
SUBMENU,
TOGGLE,
SLIDER,
CONFIRMATION
}
/// We should hold a list of SubMenus.
/// The currently selected SubMenu should also have an associated list with necessary information.
/// We can convert the option types (Toggle, OnOff, Slider) to lists
@@ -28,11 +36,12 @@ pub struct App<'a> {
pub menu_items: HashMap<&'a str, MultiStatefulList<SubMenu<'a>>>,
pub selected_sub_menu_toggles: MultiStatefulList<Toggle<'a>>,
pub selected_sub_menu_slider: DoubleEndedGauge,
pub outer_list: bool,
pub page: AppPage,
pub default_menu: (UiMenu<'a>, String),
}
impl<'a> App<'a> {
pub fn new(menu: UiMenu<'a>) -> App<'a> {
pub fn new(menu: UiMenu<'a>, default_menu: (UiMenu<'a>, String)) -> App<'a> {
let num_lists = 3;
let mut menu_items_stateful = HashMap::new();
@@ -48,7 +57,8 @@ impl<'a> App<'a> {
menu_items: menu_items_stateful,
selected_sub_menu_toggles: MultiStatefulList::with_items(vec![], 0),
selected_sub_menu_slider: DoubleEndedGauge::new(),
outer_list: true,
page: AppPage::SUBMENU,
default_menu: default_menu
};
app.set_sub_menu_items();
app
@@ -269,7 +279,7 @@ impl<'a> App<'a> {
}
/// Different behavior depending on the current menu location
/// Outer list: Sets self.outer_list to false
/// Submenu list: Enters toggle or slider submenu
/// Toggle submenu: Toggles the selected submenu toggle in self.selected_sub_menu_toggles and in the actual SubMenu struct
/// Slider submenu: Swaps hover/selected state. Updates the actual SubMenu struct if going from Selected -> Hover
pub fn on_a(&mut self) {
@@ -288,14 +298,14 @@ impl<'a> App<'a> {
.items
.get_mut(list_idx)
.unwrap();
if self.outer_list {
self.outer_list = false;
if self.page == AppPage::SUBMENU {
match SubMenuType::from_str(selected_sub_menu._type) {
// Need to change the slider state to MinHover so the slider shows up initially
SubMenuType::SLIDER => {
self.page = AppPage::SLIDER;
self.selected_sub_menu_slider.state = GaugeState::MinHover;
}
_ => {}
SubMenuType::TOGGLE => self.page = AppPage::TOGGLE
}
} else {
match SubMenuType::from_str(selected_sub_menu._type) {
@@ -371,9 +381,9 @@ impl<'a> App<'a> {
}
/// Different behavior depending on the current menu location
/// Outer list: None
/// Toggle submenu: Sets self.outer_list to true
/// Slider submenu: If in a selected state, then commit changes and change to hover. Else set self.outer_list to true
/// Submenu selection: None
/// Toggle submenu: Sets page to submenu selection
/// Slider submenu: If in a selected state, then commit changes and change to hover. Else set page to submenu selection
pub fn on_b(&mut self) {
let tab_selected = self
.tabs
@@ -417,26 +427,55 @@ impl<'a> App<'a> {
},
_ => {}
}
self.outer_list = true;
self.page = AppPage::SUBMENU;
self.set_sub_menu_items();
}
/// Save defaults command
pub fn on_x(&mut self) {
if self.page == AppPage::SUBMENU {
let json = self.to_json();
unsafe {
self.default_menu = (ui_menu(serde_json::from_str::<TrainingModpackMenu>(&json).unwrap()), json);
}
}
}
/// Reset current submenu to defaults
pub fn on_l(&mut self) {
if self.outer_list {
if self.page == AppPage::TOGGLE || self.page == AppPage::SLIDER {
let json = self.to_json();
let mut json_value = serde_json::from_str::<serde_json::Value>(&json).unwrap();
let selected_sub_menu= self.sub_menu_selected();
let id = selected_sub_menu.submenu_id;
let default_json_value = serde_json::from_str::<serde_json::Value>(&self.default_menu.1).unwrap();
*json_value.get_mut(id).unwrap() = default_json_value.get(id).unwrap().clone();
let new_menu = serde_json::from_value::<TrainingModpackMenu>(json_value).unwrap();
*self = App::new(unsafe { ui_menu(new_menu) }, self.default_menu.clone());
}
}
/// Reset all menus to defaults
pub fn on_r(&mut self) {
*self = App::new(self.default_menu.0.clone(), self.default_menu.clone());
}
pub fn on_zl(&mut self) {
if self.page == AppPage::SUBMENU {
self.tabs.previous();
self.set_sub_menu_items();
}
}
pub fn on_r(&mut self) {
if self.outer_list {
pub fn on_zr(&mut self) {
if self.page == AppPage::SUBMENU {
self.tabs.next();
self.set_sub_menu_items();
}
}
pub fn on_up(&mut self) {
if self.outer_list {
if self.page == AppPage::SUBMENU {
self.menu_items
.get_mut(
self.tabs
@@ -447,13 +486,13 @@ impl<'a> App<'a> {
.unwrap()
.previous();
self.set_sub_menu_items();
} else {
} else if self.page == AppPage::TOGGLE || self.page == AppPage::SLIDER {
self.sub_menu_previous();
}
}
pub fn on_down(&mut self) {
if self.outer_list {
if self.page == AppPage::SUBMENU {
self.menu_items
.get_mut(
self.tabs
@@ -464,13 +503,13 @@ impl<'a> App<'a> {
.unwrap()
.next();
self.set_sub_menu_items();
} else {
} else if self.page == AppPage::TOGGLE || self.page == AppPage::SLIDER {
self.sub_menu_next();
}
}
pub fn on_left(&mut self) {
if self.outer_list {
if self.page == AppPage::SUBMENU {
self.menu_items
.get_mut(
self.tabs
@@ -481,13 +520,13 @@ impl<'a> App<'a> {
.unwrap()
.previous_list();
self.set_sub_menu_items();
} else {
} else if self.page == AppPage::TOGGLE || self.page == AppPage::SLIDER {
self.sub_menu_previous_list();
}
}
pub fn on_right(&mut self) {
if self.outer_list {
if self.page == AppPage::SUBMENU {
self.menu_items
.get_mut(
self.tabs
@@ -498,13 +537,221 @@ impl<'a> App<'a> {
.unwrap()
.next_list();
self.set_sub_menu_items();
} else {
} else if self.page == AppPage::TOGGLE || self.page == AppPage::SLIDER {
self.sub_menu_next_list();
}
}
/// Returns JSON representation of current menu settings
pub fn to_json(&self) -> String {
let mut settings = Map::new();
for key in self.menu_items.keys() {
for list in &self.menu_items.get(key).unwrap().lists {
for sub_menu in &list.items {
if !sub_menu.toggles.is_empty() {
let val: u32 = sub_menu
.toggles
.iter()
.filter(|t| t.checked)
.map(|t| t.toggle_value)
.sum();
settings.insert(sub_menu.submenu_id.to_string(), json!(val));
} else if sub_menu.slider.is_some() {
let s: &Slider = sub_menu.slider.as_ref().unwrap();
let val: Vec<u32> = vec![s.selected_min, s.selected_max];
settings.insert(sub_menu.submenu_id.to_string(), json!(val));
} else {
panic!("Could not collect settings for {:?}", sub_menu.submenu_id);
}
}
}
}
serde_json::to_string(&settings).unwrap()
}
/// Returns the current menu selections and the default menu selections.
pub fn get_menu_selections(&self) -> String {
serde_json::to_string(
&MenuJsonStruct {
menu: serde_json::from_str(self.to_json().as_str()).unwrap(),
defaults_menu: serde_json::from_str(self.default_menu.1.clone().as_str()).unwrap(),
}).unwrap()
}
}
pub fn ui<B: Backend>(f: &mut Frame<B>, app: &mut App) -> String {
fn render_submenu_page<B: Backend>(f: &mut Frame<B>, app: &mut App, list_chunks: Vec<Rect>, help_chunk: Rect) {
let tab_selected = app.tab_selected();
let mut item_help = None;
for (list_section, stateful_list) in app
.menu_items
.get(tab_selected)
.unwrap()
.lists
.iter()
.enumerate()
{
let items: Vec<ListItem> = stateful_list
.items
.iter()
.map(|i| {
let lines = vec![Spans::from(if stateful_list.state.selected().is_some() {
i.submenu_title.to_owned()
} else {
" ".to_owned() + i.submenu_title
})];
ListItem::new(lines).style(Style::default().fg(Color::White))
})
.collect();
let list = List::new(items)
.block(
Block::default()
.title(if list_section == 0 { "Options" } else { "" })
.style(Style::default().fg(Color::LightRed)),
)
.highlight_style(
Style::default()
.fg(Color::Green)
.add_modifier(Modifier::BOLD),
)
.highlight_symbol(">> ");
let mut state = stateful_list.state.clone();
if state.selected().is_some() {
item_help = Some(stateful_list.items[state.selected().unwrap()].help_text);
}
f.render_stateful_widget(list, list_chunks[list_section], &mut state);
}
let help_paragraph = Paragraph::new(
item_help.unwrap_or("").replace('\"', "")
+ "\nA: Enter sub-menu | B: Exit menu | ZL/ZR: Next tab | X: Save Defaults",
)
.style(Style::default().fg(Color::Cyan));
f.render_widget(help_paragraph, help_chunk);
}
pub fn render_toggle_page<B: Backend>(f: &mut Frame<B>, app: &mut App, list_chunks: Vec<Rect>, help_chunk: Rect) {
let (title, help_text, mut sub_menu_str_lists) = app.sub_menu_strs_and_states();
for list_section in 0..sub_menu_str_lists.len() {
let sub_menu_str = sub_menu_str_lists[list_section].0.clone();
let sub_menu_state = &mut sub_menu_str_lists[list_section].1;
let values_items: Vec<ListItem> = sub_menu_str
.iter()
.map(|s| {
ListItem::new(vec![Spans::from(
(if s.0 { "X " } else { " " }).to_owned() + s.1,
)])
})
.collect();
let values_list = List::new(values_items)
.block(Block::default().title(if list_section == 0 { title } else { "" }))
.start_corner(Corner::TopLeft)
.highlight_style(
Style::default()
.fg(Color::LightGreen)
.add_modifier(Modifier::BOLD),
)
.highlight_symbol(">> ");
f.render_stateful_widget(values_list, list_chunks[list_section], sub_menu_state);
}
let help_paragraph = Paragraph::new(
help_text.replace('\"', "") + "\nA: Select toggle | B: Exit submenu | X: Reset to defaults",
)
.style(Style::default().fg(Color::Cyan));
f.render_widget(help_paragraph, help_chunk);
}
pub fn render_slider_page<B: Backend>(f: &mut Frame<B>, app: &mut App, vertical_chunk: Rect, help_chunk: Rect) {
let (_title, help_text, gauge_vals) = app.sub_menu_strs_for_slider();
let abs_min = gauge_vals.abs_min;
let abs_max = gauge_vals.abs_max;
let selected_min = gauge_vals.selected_min;
let selected_max = gauge_vals.selected_max;
let lbl_ratio = 0.95; // Needed so that the upper limit label is visible
let constraints = [
Constraint::Ratio((lbl_ratio * (selected_min-abs_min) as f32) as u32, abs_max-abs_min),
Constraint::Ratio((lbl_ratio * (selected_max-selected_min) as f32) as u32, abs_max-abs_min),
Constraint::Ratio((lbl_ratio * (abs_max-selected_max) as f32) as u32, abs_max-abs_min),
Constraint::Min(3), // For upper limit label
];
let gauge_chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints(constraints)
.split(vertical_chunk);
let slider_lbls = [
abs_min,
selected_min,
selected_max,
abs_max,
];
for (idx, lbl) in slider_lbls.iter().enumerate() {
let mut line_set = tui::symbols::line::NORMAL;
line_set.horizontal = "-";
let mut gauge = LineGauge::default()
.ratio(1.0)
.label(format!("{}", lbl))
.style(Style::default().fg(Color::White))
.line_set(line_set)
.gauge_style(Style::default().fg(Color::White).bg(Color::Black));
if idx == 1 {
// Slider between selected_min and selected_max
match gauge_vals.state {
GaugeState::MinHover => {
gauge = gauge.style(Style::default().fg(Color::Red))
}
GaugeState::MinSelected => {
gauge = gauge.style(Style::default().fg(Color::Green))
}
_ => {}
}
gauge = gauge.gauge_style(Style::default().fg(Color::Yellow).bg(Color::Black));
} else if idx == 2 {
// Slider between selected_max and abs_max
match gauge_vals.state {
GaugeState::MaxHover => {
gauge = gauge.style(Style::default().fg(Color::Red))
}
GaugeState::MaxSelected => {
gauge = gauge.style(Style::default().fg(Color::Green))
}
_ => {}
}
} else if idx == 3 {
// Slider for abs_max
// We only want the label to show, so set the line character to " "
let mut line_set = tui::symbols::line::NORMAL;
line_set.horizontal = " ";
gauge = gauge.line_set(line_set);
// For some reason, the selected_max slider displays on top
// So we need to change the abs_max slider styling to match
// If the selected_max is close enough to the abs_max
if (selected_max as f32 / abs_max as f32) > 0.95 {
gauge = gauge.style(match gauge_vals.state {
GaugeState::MaxHover => Style::default().fg(Color::Red),
GaugeState::MaxSelected => Style::default().fg(Color::Green),
_ => Style::default(),
})
}
}
f.render_widget(gauge, gauge_chunks[idx]);
}
let help_paragraph = Paragraph::new(
help_text.replace('\"', "") + "\nA: Select toggle | B: Exit submenu | X: Reset to defaults",
)
.style(Style::default().fg(Color::Cyan));
f.render_widget(help_paragraph, help_chunk);
}
/// Run
pub fn ui<B: Backend>(f: &mut Frame<B>, app: &mut App) {
let app_tabs = &app.tabs;
let tab_selected = app_tabs.state.selected().unwrap();
let mut span_selected = Spans::default();
@@ -611,202 +858,10 @@ pub fn ui<B: Backend>(f: &mut Frame<B>, app: &mut App) -> String {
f.render_widget(tabs, vertical_chunks[0]);
if app.outer_list {
let tab_selected = app.tab_selected();
let mut item_help = None;
for (list_section, stateful_list) in app
.menu_items
.get(tab_selected)
.unwrap()
.lists
.iter()
.enumerate()
{
let items: Vec<ListItem> = stateful_list
.items
.iter()
.map(|i| {
let lines = vec![Spans::from(if stateful_list.state.selected().is_some() {
i.submenu_title.to_owned()
} else {
" ".to_owned() + i.submenu_title
})];
ListItem::new(lines).style(Style::default().fg(Color::White))
})
.collect();
let list = List::new(items)
.block(
Block::default()
.title(if list_section == 0 { "Options" } else { "" })
.style(Style::default().fg(Color::LightRed)),
)
.highlight_style(
Style::default()
.fg(Color::Green)
.add_modifier(Modifier::BOLD),
)
.highlight_symbol(">> ");
let mut state = stateful_list.state.clone();
if state.selected().is_some() {
item_help = Some(stateful_list.items[state.selected().unwrap()].help_text);
}
f.render_stateful_widget(list, list_chunks[list_section], &mut state);
}
// TODO: Add Save Defaults
let help_paragraph = Paragraph::new(
item_help.unwrap_or("").replace('\"', "")
+ "\nA: Enter sub-menu | B: Exit menu | ZL/ZR: Next tab",
)
.style(Style::default().fg(Color::Cyan));
f.render_widget(help_paragraph, vertical_chunks[2]);
} else {
if matches!(app.selected_sub_menu_slider.state, GaugeState::None) {
let (title, help_text, mut sub_menu_str_lists) = app.sub_menu_strs_and_states();
for list_section in 0..sub_menu_str_lists.len() {
let sub_menu_str = sub_menu_str_lists[list_section].0.clone();
let sub_menu_state = &mut sub_menu_str_lists[list_section].1;
let values_items: Vec<ListItem> = sub_menu_str
.iter()
.map(|s| {
ListItem::new(vec![Spans::from(
(if s.0 { "X " } else { " " }).to_owned() + s.1,
)])
})
.collect();
let values_list = List::new(values_items)
.block(Block::default().title(if list_section == 0 { title } else { "" }))
.start_corner(Corner::TopLeft)
.highlight_style(
Style::default()
.fg(Color::LightGreen)
.add_modifier(Modifier::BOLD),
)
.highlight_symbol(">> ");
f.render_stateful_widget(values_list, list_chunks[list_section], sub_menu_state);
}
let help_paragraph = Paragraph::new(
help_text.replace('\"', "") + "\nA: Select toggle | B: Exit submenu",
)
.style(Style::default().fg(Color::Cyan));
f.render_widget(help_paragraph, vertical_chunks[2]);
} else {
let (_title, help_text, gauge_vals) = app.sub_menu_strs_for_slider();
let abs_min = gauge_vals.abs_min;
let abs_max = gauge_vals.abs_max;
let selected_min = gauge_vals.selected_min;
let selected_max = gauge_vals.selected_max;
let lbl_ratio = 0.95; // Needed so that the upper limit label is visible
let constraints = [
Constraint::Ratio((lbl_ratio * (selected_min-abs_min) as f32) as u32, abs_max-abs_min),
Constraint::Ratio((lbl_ratio * (selected_max-selected_min) as f32) as u32, abs_max-abs_min),
Constraint::Ratio((lbl_ratio * (abs_max-selected_max) as f32) as u32, abs_max-abs_min),
Constraint::Min(3), // For upper limit label
];
let gauge_chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints(constraints)
.split(vertical_chunks[1]);
let slider_lbls = [
abs_min,
selected_min,
selected_max,
abs_max,
];
for (idx, lbl) in slider_lbls.iter().enumerate() {
let mut line_set = tui::symbols::line::NORMAL;
line_set.horizontal = "-";
let mut gauge = LineGauge::default()
.ratio(1.0)
.label(format!("{}", lbl))
.style(Style::default().fg(Color::White))
.line_set(line_set)
.gauge_style(Style::default().fg(Color::White).bg(Color::Black));
if idx == 1 {
// Slider between selected_min and selected_max
match gauge_vals.state {
GaugeState::MinHover => {
gauge = gauge.style(Style::default().fg(Color::Red))
}
GaugeState::MinSelected => {
gauge = gauge.style(Style::default().fg(Color::Green))
}
_ => {}
}
gauge = gauge.gauge_style(Style::default().fg(Color::Yellow).bg(Color::Black));
} else if idx == 2 {
// Slider between selected_max and abs_max
match gauge_vals.state {
GaugeState::MaxHover => {
gauge = gauge.style(Style::default().fg(Color::Red))
}
GaugeState::MaxSelected => {
gauge = gauge.style(Style::default().fg(Color::Green))
}
_ => {}
}
} else if idx == 3 {
// Slider for abs_max
// We only want the label to show, so set the line character to " "
let mut line_set = tui::symbols::line::NORMAL;
line_set.horizontal = " ";
gauge = gauge.line_set(line_set);
// For some reason, the selected_max slider displays on top
// So we need to change the abs_max slider styling to match
// If the selected_max is close enough to the abs_max
if (selected_max as f32 / abs_max as f32) > 0.95 {
gauge = gauge.style(match gauge_vals.state {
GaugeState::MaxHover => Style::default().fg(Color::Red),
GaugeState::MaxSelected => Style::default().fg(Color::Green),
_ => Style::default(),
})
}
}
f.render_widget(gauge, gauge_chunks[idx]);
}
let help_paragraph = Paragraph::new(
help_text.replace('\"', "") + "\nA: Select toggle | B: Exit submenu",
)
.style(Style::default().fg(Color::Cyan));
f.render_widget(help_paragraph, vertical_chunks[2]);
}
match app.page {
AppPage::SUBMENU => render_submenu_page(f, app, list_chunks, vertical_chunks[2]),
AppPage::SLIDER => render_slider_page(f, app, vertical_chunks[1], vertical_chunks[2]),
AppPage::TOGGLE => render_toggle_page(f, app, list_chunks, vertical_chunks[2]),
AppPage::CONFIRMATION => todo!()
}
// Collect settings
to_json(app)
// TODO: Add saveDefaults
}
pub fn to_json(app: &App) -> String {
let mut settings = Map::new();
for key in app.menu_items.keys() {
for list in &app.menu_items.get(key).unwrap().lists {
for sub_menu in &list.items {
if !sub_menu.toggles.is_empty() {
let val: u32 = sub_menu
.toggles
.iter()
.filter(|t| t.checked)
.map(|t| t.toggle_value)
.sum();
settings.insert(sub_menu.submenu_id.to_string(), json!(val));
} else if sub_menu.slider.is_some() {
let s: &Slider = sub_menu.slider.as_ref().unwrap();
let val: Vec<u32> = vec![s.selected_min, s.selected_max];
settings.insert(sub_menu.submenu_id.to_string(), json!(val));
} else {
panic!("Could not collect settings for {:?}", sub_menu.submenu_id);
}
}
}
}
serde_json::to_string(&settings).unwrap()
}
}

View File

@@ -17,10 +17,10 @@ use tui::Terminal;
use training_mod_consts::*;
fn test_backend_setup(ui_menu: UiMenu) -> Result<
(Terminal<training_mod_tui::TestBackend>, training_mod_tui::App),
fn test_backend_setup<'a>(ui_menu: UiMenu<'a>, menu_defaults: (UiMenu<'a>, String)) -> Result<
(Terminal<training_mod_tui::TestBackend>, training_mod_tui::App<'a>),
Box<dyn Error>> {
let app = training_mod_tui::App::new(ui_menu);
let app = training_mod_tui::App::<'a>::new(ui_menu, menu_defaults);
let backend = tui::backend::TestBackend::new(75, 15);
let terminal = Terminal::new(backend)?;
let mut state = tui::widgets::ListState::default();
@@ -30,26 +30,154 @@ fn test_backend_setup(ui_menu: UiMenu) -> Result<
}
#[test]
fn ensure_menu_retains_selections() -> Result<(), Box<dyn Error>> {
fn test_set_airdodge() -> Result<(), Box<dyn Error>> {
let menu;
let prev_menu;
let mut prev_menu;
let menu_defaults;
unsafe {
prev_menu = MENU;
menu = get_menu();
prev_menu = MENU.clone();
menu = ui_menu(MENU);
menu_defaults = (ui_menu(MENU), serde_json::to_string(&MENU).unwrap());
}
let (mut terminal, mut app) = test_backend_setup(menu)?;
let mut json_response = String::new();
let _frame_res = terminal.draw(|f| json_response = training_mod_tui::ui(f, &mut app))?;
let (_terminal, mut app) = test_backend_setup(menu, menu_defaults)?;
// Enter Mash Toggles
app.on_a();
// Set Mash Airdodge
app.on_a();
let menu_json = app.get_menu_selections();
let menu_struct = serde_json::from_str::<MenuJsonStruct>(&menu_json).unwrap();
let menu = menu_struct.menu;
let _ = menu_struct.defaults_menu;
prev_menu.mash_state.toggle(Action::AIR_DODGE);
assert_eq!(
serde_json::to_string(&prev_menu).unwrap(),
serde_json::to_string(&menu).unwrap()
);
Ok(())
}
#[test]
fn test_ensure_menu_retains_selections() -> Result<(), Box<dyn Error>> {
let menu;
let prev_menu;
let menu_defaults;
unsafe {
MENU = serde_json::from_str::<TrainingModpackMenu>(&json_response).unwrap();
// At this point, we didn't change the menu at all; we should still see all the same options.
assert_eq!(
serde_json::to_string(&prev_menu).unwrap(),
serde_json::to_string(&MENU).unwrap()
);
prev_menu = MENU.clone();
menu = ui_menu(MENU);
menu_defaults = (ui_menu(MENU), serde_json::to_string(&MENU).unwrap());
}
let (_terminal, app) = test_backend_setup(menu, menu_defaults)?;
let menu_json = app.get_menu_selections();
let menu_struct = serde_json::from_str::<MenuJsonStruct>(&menu_json).unwrap();
let menu = menu_struct.menu;
let _ = menu_struct.defaults_menu;
// At this point, we didn't change the menu at all; we should still see all the same options.
assert_eq!(
serde_json::to_string(&prev_menu).unwrap(),
serde_json::to_string(&menu).unwrap()
);
Ok(())
}
#[test]
fn test_save_and_reset_defaults() -> Result<(), Box<dyn Error>> {
let menu;
let mut prev_menu;
let menu_defaults;
unsafe {
prev_menu = MENU.clone();
menu = ui_menu(MENU);
menu_defaults = (ui_menu(MENU), serde_json::to_string(&MENU).unwrap());
}
let (_terminal, mut app) = test_backend_setup(menu, menu_defaults)?;
// Enter Mash Toggles
app.on_a();
// Set Mash Airdodge
app.on_a();
// Return to submenu selection
app.on_b();
// Save Defaults
app.on_x();
// Enter Mash Toggles again
app.on_a();
// Unset Mash Airdodge
app.on_a();
let menu_json = app.get_menu_selections();
let menu_struct = serde_json::from_str::<MenuJsonStruct>(&menu_json).unwrap();
let menu = menu_struct.menu;
let defaults_menu = menu_struct.defaults_menu;
assert_eq!(
serde_json::to_string(&prev_menu).unwrap(),
serde_json::to_string(&menu).unwrap(),
"The menu should be the same as how we started"
);
prev_menu.mash_state.toggle(Action::AIR_DODGE);
assert_eq!(
serde_json::to_string(&prev_menu).unwrap(),
serde_json::to_string(&defaults_menu).unwrap(),
"The defaults menu should have Mash Airdodge"
);
// Reset current menu alone to defaults
app.on_l();
let menu_json = app.get_menu_selections();
let menu_struct = serde_json::from_str::<MenuJsonStruct>(&menu_json).unwrap();
let menu = menu_struct.menu;
let _ = menu_struct.defaults_menu;
assert_eq!(
serde_json::to_string(&prev_menu).unwrap(),
serde_json::to_string(&menu).unwrap(),
"The menu should have Mash Airdodge"
);
// Enter Mash Toggles
app.on_a();
// Unset Mash Airdodge
app.on_a();
// Return to submenu selection
app.on_b();
// Go down to Followup Toggles
app.on_down();
// Enter Followup Toggles
app.on_a();
// Go down and set Jump
app.on_down();
app.on_a();
// Return to submenu selection
app.on_b();
// Save defaults
app.on_x();
// Go back in and unset Jump
app.on_a();
app.on_down();
app.on_a();
// Return to submenu selection
app.on_b();
// Reset all to defaults
app.on_r();
let menu_json = app.get_menu_selections();
let menu_struct = serde_json::from_str::<MenuJsonStruct>(&menu_json).unwrap();
let menu = menu_struct.menu;
let _ = menu_struct.defaults_menu;
prev_menu.mash_state.toggle(Action::AIR_DODGE);
prev_menu.follow_up.toggle(Action::JUMP);
assert_eq!(
serde_json::to_string(&prev_menu).unwrap(),
serde_json::to_string(&menu).unwrap(),
"The menu should have Mash Airdodge off and Followup Jump on"
);
Ok(())
}
@@ -57,17 +185,23 @@ fn main() -> Result<(), Box<dyn Error>> {
let args: Vec<String> = std::env::args().collect();
let inputs = args.get(1);
let menu;
let menu_defaults;
unsafe {
menu = get_menu();
menu = ui_menu(MENU);
menu_defaults = (ui_menu(MENU), serde_json::to_string(&MENU).unwrap());
}
#[cfg(not(feature = "has_terminal"))] {
let (mut terminal, mut app) = test_backend_setup(menu)?;
let (mut terminal, mut app) = test_backend_setup(menu, menu_defaults)?;
if inputs.is_some() {
inputs.unwrap().split(",").for_each(|input| {
match input.to_uppercase().as_str() {
"X" => app.on_x(),
"L" => app.on_l(),
"R" => app.on_r(),
"O" => app.on_zl(),
"P" => app.on_zr(),
"A" => app.on_a(),
"B" => app.on_b(),
"UP" => app.on_up(),
@@ -78,8 +212,8 @@ fn main() -> Result<(), Box<dyn Error>> {
}
})
}
let mut json_response = String::new();
let frame_res = terminal.draw(|f| json_response = training_mod_tui::ui(f, &mut app))?;
let frame_res = terminal.draw(|f| training_mod_tui::ui(f, &mut app))?;
let menu_json = app.get_menu_selections();
for (i, cell) in frame_res.buffer.content().iter().enumerate() {
print!("{}", cell.symbol);
@@ -89,11 +223,11 @@ fn main() -> Result<(), Box<dyn Error>> {
}
println!();
println!("json_response:\n{}", json_response);
println!("Menu:\n{menu_json}");
}
#[cfg(feature = "has_terminal")] {
let app = training_mod_tui::App::new(menu);
let app = training_mod_tui::App::new(menu, menu_defaults);
// setup terminal
enable_raw_mode()?;
@@ -117,10 +251,10 @@ fn main() -> Result<(), Box<dyn Error>> {
if let Err(err) = res {
println!("{:?}", err)
} else {
println!("JSON: {}", res.as_ref().unwrap());
println!("JSONs: {:#?}", res.as_ref().unwrap());
unsafe {
MENU = serde_json::from_str::<TrainingModpackMenu>(&res.as_ref().unwrap()).unwrap();
println!("MENU: {:#?}", MENU);
let menu = serde_json::from_str::<MenuJsonStruct>(&res.as_ref().unwrap()).unwrap();
println!("menu: {:#?}", menu);
}
}
}
@@ -135,9 +269,9 @@ fn run_app<B: tui::backend::Backend>(
tick_rate: Duration,
) -> io::Result<String> {
let mut last_tick = Instant::now();
let mut json_response = String::new();
loop {
terminal.draw(|f| json_response = training_mod_tui::ui(f, &mut app).clone())?;
terminal.draw(|f| training_mod_tui::ui(f, &mut app).clone())?;
let menu_json = app.get_menu_selections();
let timeout = tick_rate
.checked_sub(last_tick.elapsed())
@@ -146,7 +280,10 @@ fn run_app<B: tui::backend::Backend>(
if crossterm::event::poll(timeout)? {
if let Event::Key(key) = event::read()? {
match key.code {
KeyCode::Char('q') => return Ok(json_response),
KeyCode::Char('q') => return Ok(menu_json),
KeyCode::Char('x') => app.on_x(),
KeyCode::Char('p') => app.on_zr(),
KeyCode::Char('o') => app.on_zl(),
KeyCode::Char('r') => app.on_r(),
KeyCode::Char('l') => app.on_l(),
KeyCode::Left => app.on_left(),