mirror of
https://github.com/jugeeya/UltimateTrainingModpack.git
synced 2024-11-24 02:44:17 +00:00
Weighted Probability Menu Selections + TUI Backend Redesign (#655)
* Add replacement training_mod_tui crate * Begin work on converting over to byteflags * Allow StatefulTable.iter_mut() * Additional merge work * Fixing more compile errors from the merge * Replace training_mod_tui with training_mod_tui_2, update byteflags dependency for random selection * Fix button_config, hero buffs, caps issues. Move button logic into App * Restore some functions that I cut too agressively * Move to_vec into byteflags crate * Fix src/training/ui * Set `_` match arms on byteflags to `unreachable!()` * Fix final few compiler errors * Run formatter * Reconsider whether unreachable parts are actually unreachable * Adjust logging and remove dead code * Fix some menu bugs * Adjust icon sizes * Separate checkmark from is_single_option * Allow fast increment / decrement of slider handles with up/down * Allow resetting current menu * Change button behavior: `R/Z` now resets either the whole menu or the selected submenu depending on app.page, and `Y` now clears the current toggle so you don't have to press A six times. Only show keyhelp for `Y` if its relevant. * Remove unneeded command line interface * Take care of some minor nomenclature TODO's * Increase stack size for menu::load_from_file() to avoid crashes * Implement confirmation page before restoring from defaults * Run rustfmt * Fix warnings
This commit is contained in:
parent
3130b5ad30
commit
65f87df1e0
48 changed files with 4922 additions and 4369 deletions
|
@ -41,10 +41,11 @@ byte-unit = "4.0.18"
|
|||
zip = { version = "0.6", default-features = false, features = ["deflate"] }
|
||||
anyhow = "1.0.72"
|
||||
|
||||
|
||||
[patch.crates-io]
|
||||
native-tls = { git = "https://github.com/skyline-rs/rust-native-tls", branch = "switch-timeout-panic" }
|
||||
nnsdk = { git = "https://github.com/ultimate-research/nnsdk-rs" }
|
||||
rand = { git = "https://github.com/skyline-rs/rand" }
|
||||
getrandom = { git = "https://github.com/Raytwo/getrandom" }
|
||||
|
||||
[profile.dev]
|
||||
panic = "abort"
|
||||
|
|
|
@ -45,7 +45,7 @@ pub fn button_mapping(
|
|||
ButtonConfig::MINUS => b.minus(),
|
||||
ButtonConfig::LSTICK => b.stick_l(),
|
||||
ButtonConfig::RSTICK => b.stick_r(),
|
||||
_ => false,
|
||||
_ => panic!("Invalid value in button_mapping: {}", button_config),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -166,7 +166,11 @@ pub enum ButtonCombo {
|
|||
InputPlayback,
|
||||
}
|
||||
|
||||
pub const DEFAULT_OPEN_MENU_CONFIG: ButtonConfig = ButtonConfig::B.union(ButtonConfig::DPAD_UP);
|
||||
pub const DEFAULT_OPEN_MENU_CONFIG: ButtonConfig = ButtonConfig {
|
||||
B: 1,
|
||||
DPAD_UP: 1,
|
||||
..ButtonConfig::empty()
|
||||
};
|
||||
|
||||
unsafe fn get_combo_keys(combo: ButtonCombo) -> ButtonConfig {
|
||||
match combo {
|
||||
|
@ -196,14 +200,14 @@ fn _combo_passes(p1_controller: Controller, combo: ButtonCombo) -> bool {
|
|||
let combo_keys = get_combo_keys(combo).to_vec();
|
||||
let mut this_combo_passes = false;
|
||||
|
||||
for hold_button in &combo_keys[..] {
|
||||
for hold_button in combo_keys.iter() {
|
||||
if button_mapping(
|
||||
*hold_button,
|
||||
p1_controller.style,
|
||||
p1_controller.current_buttons,
|
||||
) && combo_keys
|
||||
.iter()
|
||||
.filter(|press_button| **press_button != *hold_button)
|
||||
.filter(|press_button| press_button != &hold_button)
|
||||
.all(|press_button| {
|
||||
button_mapping(*press_button, p1_controller.style, p1_controller.just_down)
|
||||
})
|
||||
|
@ -238,7 +242,7 @@ pub fn handle_final_input_mapping(player_idx: i32, controller_struct: &mut SomeC
|
|||
let mut start_menu_request = false;
|
||||
|
||||
let menu_close_wait_frame = frame_counter::get_frame_count(*menu::MENU_CLOSE_FRAME_COUNTER);
|
||||
if unsafe { MENU.menu_open_start_press == OnOff::On } {
|
||||
if unsafe { MENU.menu_open_start_press == OnOff::ON } {
|
||||
let start_hold_frames = &mut *START_HOLD_FRAMES.lock();
|
||||
if p1_controller.current_buttons.plus() {
|
||||
*start_hold_frames += 1;
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
#![allow(dead_code)] // TODO: Yeah don't do this
|
||||
use crate::extra_bitflag_impls;
|
||||
use bitflags::bitflags;
|
||||
use modular_bitfield::{bitfield, specifiers::*};
|
||||
|
||||
|
@ -215,14 +213,25 @@ bitflags! {
|
|||
}
|
||||
|
||||
// This requires some imports to work
|
||||
use training_mod_consts::{random_option, ToggleTrait};
|
||||
impl std::fmt::Display for Buttons {
|
||||
fn fmt(&self, _f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
extra_bitflag_impls!(Buttons);
|
||||
impl Buttons {
|
||||
pub fn to_vec(&self) -> Vec<Buttons> {
|
||||
// Reimplemented for bitflags
|
||||
let mut vec = Vec::<Buttons>::new();
|
||||
let mut field = Buttons::from_bits_truncate(self.bits);
|
||||
while !field.is_empty() {
|
||||
let flag = Buttons::from_bits(1u32 << field.bits.trailing_zeros()).unwrap();
|
||||
field -= flag;
|
||||
vec.push(flag);
|
||||
}
|
||||
vec
|
||||
}
|
||||
}
|
||||
|
||||
// Controller class used internally by the game
|
||||
#[derive(Debug, Default, Copy, Clone)]
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
use once_cell::sync::Lazy;
|
||||
use std::collections::HashMap;
|
||||
use std::fs;
|
||||
use std::io::BufReader;
|
||||
|
||||
use lazy_static::lazy_static;
|
||||
use parking_lot::Mutex;
|
||||
use skyline::nn::hid::GetNpadStyleSet;
|
||||
use training_mod_consts::MenuJsonStruct;
|
||||
use training_mod_consts::{create_app, MenuJsonStruct};
|
||||
use training_mod_tui::AppPage;
|
||||
|
||||
use crate::common::button_config::button_mapping;
|
||||
|
@ -24,11 +25,15 @@ pub unsafe fn menu_condition() -> bool {
|
|||
}
|
||||
|
||||
pub fn load_from_file() {
|
||||
// Note that this function requires a larger stack size
|
||||
// With the switch default, it'll crash w/o a helpful error message
|
||||
info!("Checking for previous menu in {MENU_OPTIONS_PATH}...");
|
||||
let err_msg = format!("Could not read {}", MENU_OPTIONS_PATH);
|
||||
if fs::metadata(MENU_OPTIONS_PATH).is_ok() {
|
||||
let menu_conf = fs::read_to_string(MENU_OPTIONS_PATH)
|
||||
.unwrap_or_else(|_| panic!("Could not remove {}", MENU_OPTIONS_PATH));
|
||||
if let Ok(menu_conf_json) = serde_json::from_str::<MenuJsonStruct>(&menu_conf) {
|
||||
let menu_conf = fs::File::open(MENU_OPTIONS_PATH).expect(&err_msg);
|
||||
let reader = BufReader::new(menu_conf);
|
||||
if let Ok(menu_conf_json) = serde_json::from_reader::<BufReader<_>, MenuJsonStruct>(reader)
|
||||
{
|
||||
unsafe {
|
||||
MENU = menu_conf_json.menu;
|
||||
DEFAULTS_MENU = menu_conf_json.defaults_menu;
|
||||
|
@ -36,16 +41,22 @@ pub fn load_from_file() {
|
|||
}
|
||||
} else {
|
||||
warn!("Previous menu found but is invalid. Deleting...");
|
||||
fs::remove_file(MENU_OPTIONS_PATH).unwrap_or_else(|_| {
|
||||
panic!(
|
||||
"{} has invalid schema but could not be deleted!",
|
||||
MENU_OPTIONS_PATH
|
||||
)
|
||||
});
|
||||
let err_msg = format!(
|
||||
"{} has invalid schema but could not be deleted!",
|
||||
MENU_OPTIONS_PATH
|
||||
);
|
||||
fs::remove_file(MENU_OPTIONS_PATH).expect(&err_msg);
|
||||
}
|
||||
} else {
|
||||
info!("No previous menu file found.");
|
||||
}
|
||||
info!("Setting initial menu selections...");
|
||||
unsafe {
|
||||
let mut app = QUICK_MENU_APP.lock();
|
||||
app.serialized_default_settings =
|
||||
serde_json::to_string(&DEFAULTS_MENU).expect("Could not serialize DEFAULTS_MENU");
|
||||
app.update_all_from_json(&serde_json::to_string(&MENU).expect("Could not serialize MENU"));
|
||||
}
|
||||
}
|
||||
|
||||
pub unsafe fn set_menu_from_json(message: &str) {
|
||||
|
@ -72,6 +83,8 @@ pub unsafe fn set_menu_from_json(message: &str) {
|
|||
pub fn spawn_menu() {
|
||||
unsafe {
|
||||
QUICK_MENU_ACTIVE = true;
|
||||
let mut app = QUICK_MENU_APP.lock();
|
||||
app.page = AppPage::SUBMENU;
|
||||
*MENU_RECEIVED_INPUT.data_ptr() = true;
|
||||
}
|
||||
}
|
||||
|
@ -89,14 +102,10 @@ enum DirectionButton {
|
|||
}
|
||||
|
||||
lazy_static! {
|
||||
pub static ref QUICK_MENU_APP: Mutex<training_mod_tui::App> = Mutex::new(
|
||||
training_mod_tui::App::new(unsafe { ui_menu(MENU) }, unsafe {
|
||||
(
|
||||
ui_menu(DEFAULTS_MENU),
|
||||
serde_json::to_string(&DEFAULTS_MENU).unwrap(),
|
||||
)
|
||||
})
|
||||
);
|
||||
pub static ref QUICK_MENU_APP: Mutex<training_mod_tui::App<'static>> = Mutex::new({
|
||||
info!("Initialized lazy_static: QUICK_MENU_APP");
|
||||
unsafe { create_app() }
|
||||
});
|
||||
pub static ref P1_CONTROLLER_STYLE: Mutex<ControllerStyle> =
|
||||
Mutex::new(ControllerStyle::default());
|
||||
static ref DIRECTION_HOLD_FRAMES: Mutex<HashMap<DirectionButton, u32>> = {
|
||||
|
@ -185,43 +194,42 @@ pub fn handle_final_input_mapping(
|
|||
}
|
||||
});
|
||||
|
||||
let app = &mut *QUICK_MENU_APP.data_ptr();
|
||||
let app = &mut *QUICK_MENU_APP.data_ptr(); // TODO: Why aren't we taking a lock here?
|
||||
button_mapping(ButtonConfig::A, style, button_presses).then(|| {
|
||||
app.on_a();
|
||||
received_input = true;
|
||||
});
|
||||
button_mapping(ButtonConfig::B, style, button_presses).then(|| {
|
||||
received_input = true;
|
||||
if app.page != AppPage::SUBMENU {
|
||||
app.on_b()
|
||||
} else {
|
||||
app.on_b();
|
||||
if app.page == AppPage::CLOSE {
|
||||
// Leave menu.
|
||||
frame_counter::start_counting(*MENU_CLOSE_FRAME_COUNTER);
|
||||
QUICK_MENU_ACTIVE = false;
|
||||
let menu_json = app.get_menu_selections();
|
||||
let menu_json = app.get_serialized_settings_with_defaults();
|
||||
set_menu_from_json(&menu_json);
|
||||
EVENT_QUEUE.push(Event::menu_open(menu_json));
|
||||
}
|
||||
});
|
||||
button_mapping(ButtonConfig::X, style, button_presses).then(|| {
|
||||
app.save_defaults();
|
||||
app.on_x();
|
||||
received_input = true;
|
||||
});
|
||||
button_mapping(ButtonConfig::Y, style, button_presses).then(|| {
|
||||
app.reset_all_submenus();
|
||||
app.on_y();
|
||||
received_input = true;
|
||||
});
|
||||
|
||||
button_mapping(ButtonConfig::ZL, style, button_presses).then(|| {
|
||||
app.previous_tab();
|
||||
app.on_zl();
|
||||
received_input = true;
|
||||
});
|
||||
button_mapping(ButtonConfig::ZR, style, button_presses).then(|| {
|
||||
app.next_tab();
|
||||
app.on_zr();
|
||||
received_input = true;
|
||||
});
|
||||
button_mapping(ButtonConfig::R, style, button_presses).then(|| {
|
||||
app.reset_current_submenu();
|
||||
app.on_r();
|
||||
received_input = true;
|
||||
});
|
||||
|
||||
|
@ -263,7 +271,6 @@ pub fn handle_final_input_mapping(
|
|||
|
||||
if received_input {
|
||||
direction_hold_frames.iter_mut().for_each(|(_, f)| *f = 0);
|
||||
set_menu_from_json(&app.get_menu_selections());
|
||||
*MENU_RECEIVED_INPUT.lock() = true;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,9 +10,12 @@ use serde_json::Value;
|
|||
use zip::ZipArchive;
|
||||
|
||||
lazy_static! {
|
||||
pub static ref CURRENT_VERSION: Mutex<String> = Mutex::new(match get_current_version() {
|
||||
Ok(v) => v,
|
||||
Err(e) => panic!("Could not find current modpack version!: {}", e),
|
||||
pub static ref CURRENT_VERSION: Mutex<String> = Mutex::new({
|
||||
info!("Initialized lazy_static: CURRENT_VERSION");
|
||||
match get_current_version() {
|
||||
Ok(v) => v,
|
||||
Err(e) => panic!("Could not find current modpack version!: {}", e),
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -178,12 +181,13 @@ pub fn perform_version_check() {
|
|||
let update_policy = get_update_policy();
|
||||
info!("Update Policy is {}", update_policy);
|
||||
let mut release_to_apply = match update_policy {
|
||||
UpdatePolicy::Stable => get_release(false),
|
||||
UpdatePolicy::Beta => get_release(true),
|
||||
UpdatePolicy::Disabled => {
|
||||
UpdatePolicy::STABLE => get_release(false),
|
||||
UpdatePolicy::BETA => get_release(true),
|
||||
UpdatePolicy::DISABLED => {
|
||||
// User does not want to update at all
|
||||
Err(anyhow!("Updates are disabled per UpdatePolicy"))
|
||||
}
|
||||
_ => panic!("Invalid value in perform_version_check: {}", update_policy),
|
||||
};
|
||||
if release_to_apply.is_ok() {
|
||||
let published_at = release_to_apply.as_ref().unwrap().published_at.clone();
|
||||
|
|
|
@ -96,7 +96,7 @@ fn hazard_intercept(ctx: &skyline::hooks::InlineCtx) {
|
|||
|
||||
fn mod_handle_hazards() {
|
||||
unsafe {
|
||||
*HAZARD_FLAG_ADDRESS = (MENU.stage_hazards == OnOff::On) as u8;
|
||||
*HAZARD_FLAG_ADDRESS = (MENU.stage_hazards == OnOff::ON) as u8;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -135,7 +135,7 @@ pub unsafe fn get_command_flag_cat(module_accessor: &mut app::BattleObjectModule
|
|||
// Resume Effect AnimCMD incase we don't display hitboxes
|
||||
MotionAnimcmdModule::set_sleep_effect(module_accessor, false);
|
||||
|
||||
if MENU.hitbox_vis == OnOff::Off {
|
||||
if MENU.hitbox_vis == OnOff::OFF {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -205,7 +205,7 @@ unsafe fn mod_handle_attack(lua_state: u64) {
|
|||
|
||||
// necessary if param object fails
|
||||
// hacky way of forcing no shield damage on all hitboxes
|
||||
if MENU.shield_state == Shield::Infinite {
|
||||
if MENU.shield_state == Shield::INFINITE {
|
||||
let mut hitbox_params: Vec<L2CValue> =
|
||||
(0..36).map(|i| l2c_agent.pop_lua_stack(i + 1)).collect();
|
||||
l2c_agent.clear_lua_stack();
|
||||
|
@ -219,7 +219,7 @@ unsafe fn mod_handle_attack(lua_state: u64) {
|
|||
}
|
||||
|
||||
// Hitbox Visualization
|
||||
if MENU.hitbox_vis == OnOff::On {
|
||||
if MENU.hitbox_vis == OnOff::ON {
|
||||
// get all necessary grabbox params
|
||||
let id = l2c_agent.pop_lua_stack(1); // int
|
||||
let joint = l2c_agent.pop_lua_stack(3); // hash40
|
||||
|
@ -274,7 +274,7 @@ unsafe fn handle_catch(lua_state: u64) {
|
|||
}
|
||||
|
||||
unsafe fn mod_handle_catch(lua_state: u64) {
|
||||
if MENU.hitbox_vis == OnOff::Off {
|
||||
if MENU.hitbox_vis == OnOff::OFF {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
15
src/lib.rs
15
src/lib.rs
|
@ -18,7 +18,7 @@ use std::fs;
|
|||
use std::path::PathBuf;
|
||||
|
||||
use skyline::nro::{self, NroInfo};
|
||||
use training_mod_consts::{extra_bitflag_impls, OnOff, LEGACY_TRAINING_MODPACK_ROOT};
|
||||
use training_mod_consts::{OnOff, LEGACY_TRAINING_MODPACK_ROOT};
|
||||
|
||||
use crate::common::button_config::DEFAULT_OPEN_MENU_CONFIG;
|
||||
use crate::common::events::events_loop;
|
||||
|
@ -66,7 +66,7 @@ pub fn main() {
|
|||
skyline::error::show_error(
|
||||
69,
|
||||
"SSBU Training Modpack has panicked! Please open the details and send a screenshot to the developer, then close the game.\n",
|
||||
err_msg.as_str(),
|
||||
&err_msg,
|
||||
);
|
||||
}));
|
||||
init_logger().unwrap();
|
||||
|
@ -101,7 +101,14 @@ pub fn main() {
|
|||
});
|
||||
}
|
||||
|
||||
menu::load_from_file();
|
||||
info!("Performing saved data check...");
|
||||
let data_loader = std::thread::Builder::new()
|
||||
.stack_size(0x20000)
|
||||
.spawn(move || {
|
||||
menu::load_from_file();
|
||||
})
|
||||
.unwrap();
|
||||
let _result = data_loader.join();
|
||||
|
||||
if !is_emulator() {
|
||||
info!("Performing version check...");
|
||||
|
@ -120,7 +127,7 @@ pub fn main() {
|
|||
notification("Training Modpack".to_string(), "Welcome!".to_string(), 60);
|
||||
notification(
|
||||
"Open Menu".to_string(),
|
||||
if MENU.menu_open_start_press == OnOff::On {
|
||||
if MENU.menu_open_start_press == OnOff::ON {
|
||||
"Hold Start".to_string()
|
||||
} else {
|
||||
DEFAULT_OPEN_MENU_CONFIG.to_string()
|
||||
|
|
Binary file not shown.
|
@ -10,8 +10,8 @@ use crate::training::handle_add_limit;
|
|||
|
||||
use once_cell::sync::Lazy;
|
||||
|
||||
static mut BUFF_REMAINING_PLAYER: i32 = 0;
|
||||
static mut BUFF_REMAINING_CPU: i32 = 0;
|
||||
static mut BUFF_REMAINING_PLAYER: usize = 0;
|
||||
static mut BUFF_REMAINING_CPU: usize = 0;
|
||||
|
||||
static mut IS_BUFFING_PLAYER: bool = false;
|
||||
static mut IS_BUFFING_CPU: bool = false;
|
||||
|
@ -46,7 +46,10 @@ pub unsafe fn is_buffing_any() -> bool {
|
|||
IS_BUFFING_CPU || IS_BUFFING_PLAYER
|
||||
}
|
||||
|
||||
pub unsafe fn set_buff_rem(module_accessor: &mut app::BattleObjectModuleAccessor, new_value: i32) {
|
||||
pub unsafe fn set_buff_rem(
|
||||
module_accessor: &mut app::BattleObjectModuleAccessor,
|
||||
new_value: usize,
|
||||
) {
|
||||
if is_operation_cpu(module_accessor) {
|
||||
BUFF_REMAINING_CPU = new_value;
|
||||
return;
|
||||
|
@ -54,7 +57,7 @@ pub unsafe fn set_buff_rem(module_accessor: &mut app::BattleObjectModuleAccessor
|
|||
BUFF_REMAINING_PLAYER = new_value;
|
||||
}
|
||||
|
||||
pub unsafe fn get_buff_rem(module_accessor: &mut app::BattleObjectModuleAccessor) -> i32 {
|
||||
pub unsafe fn get_buff_rem(module_accessor: &mut app::BattleObjectModuleAccessor) -> usize {
|
||||
if is_operation_cpu(module_accessor) {
|
||||
return BUFF_REMAINING_CPU;
|
||||
}
|
||||
|
@ -76,7 +79,7 @@ pub unsafe fn handle_buffs(
|
|||
CameraModule::stop_quake(module_accessor, *CAMERA_QUAKE_KIND_M); // stops Psyche-Up quake
|
||||
CameraModule::stop_quake(module_accessor, *CAMERA_QUAKE_KIND_S); // stops Monado Art quake
|
||||
|
||||
let menu_vec = MENU.buff_state.to_vec();
|
||||
let menu_vec = MENU.buff_state;
|
||||
|
||||
if fighter_kind == *FIGHTER_KIND_BRAVE {
|
||||
return buff_hero(module_accessor, status);
|
||||
|
@ -101,11 +104,11 @@ pub unsafe fn handle_buffs(
|
|||
}
|
||||
|
||||
unsafe fn buff_hero(module_accessor: &mut app::BattleObjectModuleAccessor, status: i32) -> bool {
|
||||
let buff_vec = MENU.buff_state.hero_buffs().to_vec();
|
||||
let buff_vec: Vec<BuffOption> = MENU.buff_state.to_vec();
|
||||
if !is_buffing(module_accessor) {
|
||||
// Initial set up for spells
|
||||
start_buff(module_accessor);
|
||||
set_buff_rem(module_accessor, buff_vec.len() as i32);
|
||||
set_buff_rem(module_accessor, buff_vec.len());
|
||||
// Since it's the first step of buffing, we need to set up how many buffs there are
|
||||
}
|
||||
if get_buff_rem(module_accessor) <= 0 {
|
||||
|
@ -133,7 +136,7 @@ unsafe fn buff_hero_single(
|
|||
}
|
||||
let spell_index = get_buff_rem(module_accessor) - 1;
|
||||
// Used to get spell from our vector
|
||||
let spell_option = buff_vec.get(spell_index as usize);
|
||||
let spell_option = buff_vec.get(spell_index);
|
||||
if spell_option.is_none() {
|
||||
// There are no spells selected, or something went wrong with making the vector
|
||||
return;
|
||||
|
|
|
@ -447,15 +447,15 @@ pub unsafe fn apply_item(character_item: CharacterItem) {
|
|||
let cpu_fighter_kind = app::utility::get_kind(&mut *cpu_module_accessor);
|
||||
let character_item_num = character_item.as_idx();
|
||||
let (item_fighter_kind, variation_idx) =
|
||||
if character_item_num <= CharacterItem::PlayerVariation8.as_idx() {
|
||||
if character_item_num <= CharacterItem::PLAYER_VARIATION_8.as_idx() {
|
||||
(
|
||||
player_fighter_kind,
|
||||
(character_item_num - CharacterItem::PlayerVariation1.as_idx()) as usize,
|
||||
(character_item_num - CharacterItem::PLAYER_VARIATION_1.as_idx()),
|
||||
)
|
||||
} else {
|
||||
(
|
||||
cpu_fighter_kind,
|
||||
(character_item_num - CharacterItem::CpuVariation1.as_idx()) as usize,
|
||||
(character_item_num - CharacterItem::CPU_VARIATION_1.as_idx()),
|
||||
)
|
||||
};
|
||||
ALL_CHAR_ITEMS
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
use crate::info;
|
||||
use smash::app::{self, lua_bind::*, smashball::is_training_mode};
|
||||
use smash::lib::lua_const::*;
|
||||
|
||||
|
@ -128,7 +129,7 @@ pub unsafe fn get_current_pikmin(
|
|||
}
|
||||
};
|
||||
let held_boid = WorkModule::get_int(module_accessor, held_work_var) as u32;
|
||||
println!(", boid: {}", held_boid);
|
||||
info!(", boid: {}", held_boid);
|
||||
pikmin_boid_vec.push(held_boid);
|
||||
}
|
||||
// Next, we get the order of the following pikmin
|
||||
|
|
|
@ -47,7 +47,7 @@ unsafe fn is_actionable(module_accessor: *mut app::BattleObjectModuleAccessor) -
|
|||
fn update_frame_advantage(new_frame_adv: i32) {
|
||||
unsafe {
|
||||
FRAME_ADVANTAGE = new_frame_adv;
|
||||
if MENU.frame_advantage == OnOff::On {
|
||||
if MENU.frame_advantage == OnOff::ON {
|
||||
// Prioritize Frame Advantage over Input Recording Playback
|
||||
ui::notifications::clear_notifications("Input Recording");
|
||||
ui::notifications::clear_notifications("Frame Advantage");
|
||||
|
|
|
@ -10,7 +10,7 @@ pub unsafe fn mod_get_stick_y(module_accessor: &mut BattleObjectModuleAccessor)
|
|||
}
|
||||
let fighter_status_kind = StatusModule::status_kind(module_accessor);
|
||||
|
||||
if MENU.crouch == OnOff::On
|
||||
if MENU.crouch == OnOff::ON
|
||||
&& [
|
||||
*FIGHTER_STATUS_KIND_WAIT,
|
||||
*FIGHTER_STATUS_KIND_SQUAT,
|
||||
|
|
|
@ -1,362 +1,366 @@
|
|||
use itertools::Itertools;
|
||||
use once_cell::sync::Lazy;
|
||||
use std::collections::VecDeque;
|
||||
|
||||
use crate::common::{input::*, menu::QUICK_MENU_ACTIVE, try_get_module_accessor};
|
||||
use lazy_static::lazy_static;
|
||||
use parking_lot::Mutex;
|
||||
use skyline::nn::ui2d::ResColor;
|
||||
use smash::app::{lua_bind::*, utility};
|
||||
use training_mod_consts::{FighterId, InputDisplay, MENU};
|
||||
|
||||
use super::{frame_counter, input_record::STICK_CLAMP_MULTIPLIER};
|
||||
|
||||
const GREEN: ResColor = ResColor {
|
||||
r: 22,
|
||||
g: 156,
|
||||
b: 0,
|
||||
a: 0,
|
||||
};
|
||||
|
||||
const RED: ResColor = ResColor {
|
||||
r: 153,
|
||||
g: 10,
|
||||
b: 10,
|
||||
a: 0,
|
||||
};
|
||||
|
||||
const CYAN: ResColor = ResColor {
|
||||
r: 0,
|
||||
g: 255,
|
||||
b: 255,
|
||||
a: 0,
|
||||
};
|
||||
|
||||
const BLUE: ResColor = ResColor {
|
||||
r: 0,
|
||||
g: 40,
|
||||
b: 108,
|
||||
a: 0,
|
||||
};
|
||||
|
||||
const PURPLE: ResColor = ResColor {
|
||||
r: 100,
|
||||
g: 66,
|
||||
b: 202,
|
||||
a: 0,
|
||||
};
|
||||
|
||||
pub const YELLOW: ResColor = ResColor {
|
||||
r: 230,
|
||||
g: 180,
|
||||
b: 14,
|
||||
a: 0,
|
||||
};
|
||||
|
||||
pub const WHITE: ResColor = ResColor {
|
||||
r: 255,
|
||||
g: 255,
|
||||
b: 255,
|
||||
a: 0,
|
||||
};
|
||||
|
||||
pub static PER_LOG_FRAME_COUNTER: Lazy<usize> =
|
||||
Lazy::new(|| frame_counter::register_counter(frame_counter::FrameCounterType::InGameNoReset));
|
||||
pub static OVERALL_FRAME_COUNTER: Lazy<usize> =
|
||||
Lazy::new(|| frame_counter::register_counter(frame_counter::FrameCounterType::InGameNoReset));
|
||||
|
||||
pub const NUM_LOGS: usize = 15;
|
||||
pub static mut DRAW_LOG_BASE_IDX: Lazy<Mutex<usize>> = Lazy::new(|| Mutex::new(0));
|
||||
|
||||
#[derive(PartialEq, Eq, Debug, Copy, Clone)]
|
||||
pub enum DirectionStrength {
|
||||
None,
|
||||
Weak,
|
||||
// Strong,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Default)]
|
||||
pub struct InputLog {
|
||||
pub ttl: u32,
|
||||
pub frames: u32,
|
||||
pub overall_frame: u32,
|
||||
pub raw_inputs: Controller,
|
||||
pub smash_inputs: MappedInputs,
|
||||
pub status: i32,
|
||||
pub fighter_kind: i32,
|
||||
}
|
||||
|
||||
impl PartialEq for InputLog {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.frames == other.frames && !self.is_different(other)
|
||||
}
|
||||
}
|
||||
impl Eq for InputLog {}
|
||||
|
||||
const WALK_THRESHOLD_X: i8 = 20;
|
||||
const _DASH_THRESHOLD_X: i8 = 102;
|
||||
const DEADZONE_THRESHOLD_Y: i8 = 30;
|
||||
const _TAP_JUMP_THRESHOLD_Y: i8 = 90;
|
||||
|
||||
fn bin_stick_values(x: i8, y: i8) -> (DirectionStrength, f32) {
|
||||
(
|
||||
// TODO
|
||||
DirectionStrength::Weak,
|
||||
match (x, y) {
|
||||
// X only
|
||||
(x, y) if y.abs() < DEADZONE_THRESHOLD_Y => match x {
|
||||
x if x > WALK_THRESHOLD_X => 0.0,
|
||||
x if x < -WALK_THRESHOLD_X => 180.0,
|
||||
_ => return (DirectionStrength::None, 0.0),
|
||||
},
|
||||
// Y only
|
||||
(x, y) if x.abs() < WALK_THRESHOLD_X => match y {
|
||||
y if y > DEADZONE_THRESHOLD_Y => 90.0,
|
||||
y if y < -DEADZONE_THRESHOLD_Y => 270.0,
|
||||
_ => return (DirectionStrength::None, 0.0),
|
||||
},
|
||||
// Positive Y
|
||||
(x, y) if y > DEADZONE_THRESHOLD_Y => match x {
|
||||
x if x > WALK_THRESHOLD_X => 45.0,
|
||||
x if x < -WALK_THRESHOLD_X => 135.0,
|
||||
_ => return (DirectionStrength::Weak, 90.0),
|
||||
},
|
||||
// Negative Y
|
||||
(x, y) if y < DEADZONE_THRESHOLD_Y => match x {
|
||||
x if x > WALK_THRESHOLD_X => 315.0,
|
||||
x if x < -WALK_THRESHOLD_X => 225.0,
|
||||
_ => return (DirectionStrength::Weak, 270.0),
|
||||
},
|
||||
_ => return (DirectionStrength::None, 0.0),
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
impl InputLog {
|
||||
pub fn is_different(&self, other: &InputLog) -> bool {
|
||||
unsafe {
|
||||
match MENU.input_display {
|
||||
InputDisplay::Smash => self.is_smash_different(other),
|
||||
InputDisplay::Raw => self.is_raw_different(other),
|
||||
InputDisplay::None => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn binned_lstick(&self) -> (DirectionStrength, f32) {
|
||||
unsafe {
|
||||
match MENU.input_display {
|
||||
InputDisplay::Smash => self.smash_binned_lstick(),
|
||||
InputDisplay::Raw => self.raw_binned_lstick(),
|
||||
InputDisplay::None => panic!("Invalid input display to log"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn binned_rstick(&self) -> (DirectionStrength, f32) {
|
||||
unsafe {
|
||||
match MENU.input_display {
|
||||
InputDisplay::Smash => self.smash_binned_rstick(),
|
||||
InputDisplay::Raw => self.raw_binned_rstick(),
|
||||
InputDisplay::None => panic!("Invalid input display to log"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn button_icons(&self) -> VecDeque<(&str, ResColor)> {
|
||||
unsafe {
|
||||
match MENU.input_display {
|
||||
InputDisplay::Smash => self.smash_button_icons(),
|
||||
InputDisplay::Raw => self.raw_button_icons(),
|
||||
InputDisplay::None => panic!("Invalid input display to log"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn smash_button_icons(&self) -> VecDeque<(&str, ResColor)> {
|
||||
self.smash_inputs
|
||||
.buttons
|
||||
.to_vec()
|
||||
.iter()
|
||||
.filter_map(|button| {
|
||||
Some(match *button {
|
||||
Buttons::ATTACK | Buttons::ATTACK_RAW => ("a", GREEN),
|
||||
Buttons::SPECIAL | Buttons::SPECIAL_RAW2 => ("b", RED),
|
||||
Buttons::JUMP => ("x", CYAN),
|
||||
Buttons::GUARD | Buttons::GUARD_HOLD => ("lb", BLUE),
|
||||
Buttons::CATCH => ("zr", PURPLE),
|
||||
Buttons::STOCK_SHARE => ("plus", WHITE),
|
||||
Buttons::APPEAL_HI => ("dpad_up", WHITE),
|
||||
Buttons::APPEAL_LW => ("dpad_down", WHITE),
|
||||
Buttons::APPEAL_SL => ("dpad_right", WHITE),
|
||||
Buttons::APPEAL_SR => ("dpad_left", WHITE),
|
||||
_ => return None,
|
||||
})
|
||||
})
|
||||
.unique_by(|(s, _)| *s)
|
||||
.collect::<VecDeque<(&str, ResColor)>>()
|
||||
}
|
||||
|
||||
fn raw_button_icons(&self) -> VecDeque<(&str, ResColor)> {
|
||||
let buttons = self.raw_inputs.current_buttons;
|
||||
let mut icons = VecDeque::new();
|
||||
if buttons.a() {
|
||||
icons.push_front(("a", GREEN));
|
||||
}
|
||||
if buttons.b() {
|
||||
icons.push_front(("b", RED));
|
||||
}
|
||||
if buttons.x() {
|
||||
icons.push_front(("x", CYAN));
|
||||
}
|
||||
if buttons.y() {
|
||||
icons.push_front(("y", CYAN));
|
||||
}
|
||||
if buttons.l() || buttons.real_digital_l() {
|
||||
icons.push_front(("lb", BLUE));
|
||||
}
|
||||
if buttons.r() || buttons.real_digital_r() {
|
||||
icons.push_front(("rb", BLUE));
|
||||
}
|
||||
if buttons.zl() {
|
||||
icons.push_front(("zl", PURPLE));
|
||||
}
|
||||
if buttons.zr() {
|
||||
icons.push_front(("zr", PURPLE));
|
||||
}
|
||||
if buttons.plus() {
|
||||
icons.push_front(("plus", WHITE));
|
||||
}
|
||||
if buttons.minus() {
|
||||
icons.push_front(("minus", WHITE));
|
||||
}
|
||||
if buttons.dpad_up() {
|
||||
icons.push_front(("dpad_up", WHITE));
|
||||
}
|
||||
if buttons.dpad_down() {
|
||||
icons.push_front(("dpad_down", WHITE));
|
||||
}
|
||||
if buttons.dpad_left() {
|
||||
icons.push_front(("dpad_left", WHITE));
|
||||
}
|
||||
if buttons.dpad_right() {
|
||||
icons.push_front(("dpad_right", WHITE));
|
||||
}
|
||||
|
||||
icons
|
||||
}
|
||||
|
||||
fn is_smash_different(&self, other: &InputLog) -> bool {
|
||||
self.smash_inputs.buttons != other.smash_inputs.buttons
|
||||
|| self.smash_binned_lstick() != other.smash_binned_lstick()
|
||||
|| self.smash_binned_rstick() != other.smash_binned_rstick()
|
||||
|| (unsafe { MENU.input_display_status.as_bool() } && self.status != other.status)
|
||||
}
|
||||
|
||||
fn smash_binned_lstick(&self) -> (DirectionStrength, f32) {
|
||||
bin_stick_values(self.smash_inputs.lstick_x, self.smash_inputs.lstick_y)
|
||||
}
|
||||
|
||||
fn smash_binned_rstick(&self) -> (DirectionStrength, f32) {
|
||||
bin_stick_values(self.smash_inputs.rstick_x, self.smash_inputs.rstick_y)
|
||||
}
|
||||
|
||||
fn is_raw_different(&self, other: &InputLog) -> bool {
|
||||
self.raw_inputs.current_buttons != other.raw_inputs.current_buttons
|
||||
|| self.raw_binned_lstick() != other.raw_binned_lstick()
|
||||
|| self.raw_binned_rstick() != other.raw_binned_rstick()
|
||||
|| (unsafe { MENU.input_display_status.as_bool() } && self.status != other.status)
|
||||
}
|
||||
|
||||
fn raw_binned_lstick(&self) -> (DirectionStrength, f32) {
|
||||
let x = (self.raw_inputs.left_stick_x / STICK_CLAMP_MULTIPLIER) as i8;
|
||||
let y = (self.raw_inputs.left_stick_y / STICK_CLAMP_MULTIPLIER) as i8;
|
||||
bin_stick_values(x, y)
|
||||
}
|
||||
|
||||
fn raw_binned_rstick(&self) -> (DirectionStrength, f32) {
|
||||
let x = (self.raw_inputs.right_stick_x / STICK_CLAMP_MULTIPLIER) as i8;
|
||||
let y = (self.raw_inputs.right_stick_y / STICK_CLAMP_MULTIPLIER) as i8;
|
||||
bin_stick_values(x, y)
|
||||
}
|
||||
}
|
||||
|
||||
fn insert_in_place<T>(array: &mut [T], value: T, index: usize) {
|
||||
array[index..].rotate_right(1);
|
||||
array[index] = value;
|
||||
}
|
||||
|
||||
fn insert_in_front<T>(array: &mut [T], value: T) {
|
||||
insert_in_place(array, value, 0);
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
pub static ref P1_INPUT_LOGS: Mutex<[InputLog; NUM_LOGS]> =
|
||||
Mutex::new([InputLog::default(); NUM_LOGS]);
|
||||
}
|
||||
|
||||
pub fn handle_final_input_mapping(
|
||||
player_idx: i32,
|
||||
controller_struct: &SomeControllerStruct,
|
||||
out: *mut MappedInputs,
|
||||
) {
|
||||
unsafe {
|
||||
if MENU.input_display == InputDisplay::None {
|
||||
return;
|
||||
}
|
||||
|
||||
if QUICK_MENU_ACTIVE {
|
||||
return;
|
||||
}
|
||||
|
||||
if player_idx == 0 {
|
||||
let module_accessor = try_get_module_accessor(FighterId::Player);
|
||||
if module_accessor.is_none() {
|
||||
return;
|
||||
}
|
||||
let module_accessor = module_accessor.unwrap();
|
||||
|
||||
let current_frame = frame_counter::get_frame_count(*PER_LOG_FRAME_COUNTER);
|
||||
let current_overall_frame = frame_counter::get_frame_count(*OVERALL_FRAME_COUNTER);
|
||||
// We should always be counting
|
||||
frame_counter::start_counting(*PER_LOG_FRAME_COUNTER);
|
||||
frame_counter::start_counting(*OVERALL_FRAME_COUNTER);
|
||||
|
||||
let potential_input_log = InputLog {
|
||||
ttl: 600,
|
||||
frames: 1,
|
||||
overall_frame: current_overall_frame,
|
||||
raw_inputs: *controller_struct.controller,
|
||||
smash_inputs: *out,
|
||||
status: StatusModule::status_kind(module_accessor),
|
||||
fighter_kind: utility::get_kind(&mut *module_accessor),
|
||||
};
|
||||
|
||||
let input_logs = &mut *P1_INPUT_LOGS.lock();
|
||||
let latest_input_log = input_logs.first_mut().unwrap();
|
||||
let prev_overall_frames = latest_input_log.overall_frame;
|
||||
let prev_ttl = latest_input_log.ttl;
|
||||
// Only update if we are on a new frame according to the latest log
|
||||
let is_new_frame = prev_overall_frames != current_overall_frame;
|
||||
if is_new_frame && latest_input_log.is_different(&potential_input_log) {
|
||||
frame_counter::reset_frame_count(*PER_LOG_FRAME_COUNTER);
|
||||
// We should count this frame already
|
||||
frame_counter::tick_idx(*PER_LOG_FRAME_COUNTER);
|
||||
insert_in_front(input_logs, potential_input_log);
|
||||
let draw_log_base_idx = &mut *DRAW_LOG_BASE_IDX.data_ptr();
|
||||
*draw_log_base_idx = (*draw_log_base_idx + 1) % NUM_LOGS;
|
||||
} else if is_new_frame {
|
||||
*latest_input_log = potential_input_log;
|
||||
latest_input_log.frames = std::cmp::min(current_frame, 99);
|
||||
latest_input_log.ttl = prev_ttl;
|
||||
}
|
||||
|
||||
// Decrease TTL
|
||||
for input_log in input_logs.iter_mut() {
|
||||
if input_log.ttl > 0 && is_new_frame {
|
||||
input_log.ttl -= 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
use itertools::Itertools;
|
||||
use once_cell::sync::Lazy;
|
||||
use std::collections::VecDeque;
|
||||
|
||||
use crate::common::{input::*, menu::QUICK_MENU_ACTIVE, try_get_module_accessor};
|
||||
use lazy_static::lazy_static;
|
||||
use parking_lot::Mutex;
|
||||
use skyline::nn::ui2d::ResColor;
|
||||
use smash::app::{lua_bind::*, utility};
|
||||
use training_mod_consts::{FighterId, InputDisplay, MENU};
|
||||
|
||||
use super::{frame_counter, input_record::STICK_CLAMP_MULTIPLIER};
|
||||
|
||||
const GREEN: ResColor = ResColor {
|
||||
r: 22,
|
||||
g: 156,
|
||||
b: 0,
|
||||
a: 0,
|
||||
};
|
||||
|
||||
const RED: ResColor = ResColor {
|
||||
r: 153,
|
||||
g: 10,
|
||||
b: 10,
|
||||
a: 0,
|
||||
};
|
||||
|
||||
const CYAN: ResColor = ResColor {
|
||||
r: 0,
|
||||
g: 255,
|
||||
b: 255,
|
||||
a: 0,
|
||||
};
|
||||
|
||||
const BLUE: ResColor = ResColor {
|
||||
r: 0,
|
||||
g: 40,
|
||||
b: 108,
|
||||
a: 0,
|
||||
};
|
||||
|
||||
const PURPLE: ResColor = ResColor {
|
||||
r: 100,
|
||||
g: 66,
|
||||
b: 202,
|
||||
a: 0,
|
||||
};
|
||||
|
||||
pub const YELLOW: ResColor = ResColor {
|
||||
r: 230,
|
||||
g: 180,
|
||||
b: 14,
|
||||
a: 0,
|
||||
};
|
||||
|
||||
pub const WHITE: ResColor = ResColor {
|
||||
r: 255,
|
||||
g: 255,
|
||||
b: 255,
|
||||
a: 0,
|
||||
};
|
||||
|
||||
pub static PER_LOG_FRAME_COUNTER: Lazy<usize> =
|
||||
Lazy::new(|| frame_counter::register_counter(frame_counter::FrameCounterType::InGameNoReset));
|
||||
pub static OVERALL_FRAME_COUNTER: Lazy<usize> =
|
||||
Lazy::new(|| frame_counter::register_counter(frame_counter::FrameCounterType::InGameNoReset));
|
||||
|
||||
pub const NUM_LOGS: usize = 15;
|
||||
pub static mut DRAW_LOG_BASE_IDX: Lazy<Mutex<usize>> = Lazy::new(|| Mutex::new(0));
|
||||
|
||||
#[derive(PartialEq, Eq, Debug, Copy, Clone)]
|
||||
pub enum DirectionStrength {
|
||||
None,
|
||||
Weak,
|
||||
// Strong,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Default)]
|
||||
pub struct InputLog {
|
||||
pub ttl: u32,
|
||||
pub frames: u32,
|
||||
pub overall_frame: u32,
|
||||
pub raw_inputs: Controller,
|
||||
pub smash_inputs: MappedInputs,
|
||||
pub status: i32,
|
||||
pub fighter_kind: i32,
|
||||
}
|
||||
|
||||
impl PartialEq for InputLog {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.frames == other.frames && !self.is_different(other)
|
||||
}
|
||||
}
|
||||
impl Eq for InputLog {}
|
||||
|
||||
const WALK_THRESHOLD_X: i8 = 20;
|
||||
const _DASH_THRESHOLD_X: i8 = 102;
|
||||
const DEADZONE_THRESHOLD_Y: i8 = 30;
|
||||
const _TAP_JUMP_THRESHOLD_Y: i8 = 90;
|
||||
|
||||
fn bin_stick_values(x: i8, y: i8) -> (DirectionStrength, f32) {
|
||||
(
|
||||
// TODO
|
||||
DirectionStrength::Weak,
|
||||
match (x, y) {
|
||||
// X only
|
||||
(x, y) if y.abs() < DEADZONE_THRESHOLD_Y => match x {
|
||||
x if x > WALK_THRESHOLD_X => 0.0,
|
||||
x if x < -WALK_THRESHOLD_X => 180.0,
|
||||
_ => return (DirectionStrength::None, 0.0),
|
||||
},
|
||||
// Y only
|
||||
(x, y) if x.abs() < WALK_THRESHOLD_X => match y {
|
||||
y if y > DEADZONE_THRESHOLD_Y => 90.0,
|
||||
y if y < -DEADZONE_THRESHOLD_Y => 270.0,
|
||||
_ => return (DirectionStrength::None, 0.0),
|
||||
},
|
||||
// Positive Y
|
||||
(x, y) if y > DEADZONE_THRESHOLD_Y => match x {
|
||||
x if x > WALK_THRESHOLD_X => 45.0,
|
||||
x if x < -WALK_THRESHOLD_X => 135.0,
|
||||
_ => return (DirectionStrength::Weak, 90.0),
|
||||
},
|
||||
// Negative Y
|
||||
(x, y) if y < DEADZONE_THRESHOLD_Y => match x {
|
||||
x if x > WALK_THRESHOLD_X => 315.0,
|
||||
x if x < -WALK_THRESHOLD_X => 225.0,
|
||||
_ => return (DirectionStrength::Weak, 270.0),
|
||||
},
|
||||
_ => return (DirectionStrength::None, 0.0),
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
impl InputLog {
|
||||
pub fn is_different(&self, other: &InputLog) -> bool {
|
||||
unsafe {
|
||||
match MENU.input_display {
|
||||
InputDisplay::SMASH => self.is_smash_different(other),
|
||||
InputDisplay::RAW => self.is_raw_different(other),
|
||||
InputDisplay::NONE => false,
|
||||
_ => panic!("Invalid value in is_different: {}", MENU.input_display),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn binned_lstick(&self) -> (DirectionStrength, f32) {
|
||||
unsafe {
|
||||
match MENU.input_display {
|
||||
InputDisplay::SMASH => self.smash_binned_lstick(),
|
||||
InputDisplay::RAW => self.raw_binned_lstick(),
|
||||
InputDisplay::NONE => panic!("Invalid input display to log"),
|
||||
_ => panic!("Invalid value in binned_lstick: {}", MENU.input_display),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn binned_rstick(&self) -> (DirectionStrength, f32) {
|
||||
unsafe {
|
||||
match MENU.input_display {
|
||||
InputDisplay::SMASH => self.smash_binned_rstick(),
|
||||
InputDisplay::RAW => self.raw_binned_rstick(),
|
||||
InputDisplay::NONE => panic!("Invalid input display to log"),
|
||||
_ => panic!("Invalid value in binned_rstick: {}", MENU.input_display),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn button_icons(&self) -> VecDeque<(&str, ResColor)> {
|
||||
unsafe {
|
||||
match MENU.input_display {
|
||||
InputDisplay::SMASH => self.smash_button_icons(),
|
||||
InputDisplay::RAW => self.raw_button_icons(),
|
||||
InputDisplay::NONE => panic!("Invalid input display to log"),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn smash_button_icons(&self) -> VecDeque<(&str, ResColor)> {
|
||||
self.smash_inputs
|
||||
.buttons
|
||||
.to_vec()
|
||||
.iter()
|
||||
.filter_map(|button| {
|
||||
Some(match *button {
|
||||
Buttons::ATTACK | Buttons::ATTACK_RAW => ("a", GREEN),
|
||||
Buttons::SPECIAL | Buttons::SPECIAL_RAW2 => ("b", RED),
|
||||
Buttons::JUMP => ("x", CYAN),
|
||||
Buttons::GUARD | Buttons::GUARD_HOLD => ("lb", BLUE),
|
||||
Buttons::CATCH => ("zr", PURPLE),
|
||||
Buttons::STOCK_SHARE => ("plus", WHITE),
|
||||
Buttons::APPEAL_HI => ("dpad_up", WHITE),
|
||||
Buttons::APPEAL_LW => ("dpad_down", WHITE),
|
||||
Buttons::APPEAL_SL => ("dpad_right", WHITE),
|
||||
Buttons::APPEAL_SR => ("dpad_left", WHITE),
|
||||
_ => return None,
|
||||
})
|
||||
})
|
||||
.unique_by(|(s, _)| *s)
|
||||
.collect::<VecDeque<(&str, ResColor)>>()
|
||||
}
|
||||
|
||||
fn raw_button_icons(&self) -> VecDeque<(&str, ResColor)> {
|
||||
let buttons = self.raw_inputs.current_buttons;
|
||||
let mut icons = VecDeque::new();
|
||||
if buttons.a() {
|
||||
icons.push_front(("a", GREEN));
|
||||
}
|
||||
if buttons.b() {
|
||||
icons.push_front(("b", RED));
|
||||
}
|
||||
if buttons.x() {
|
||||
icons.push_front(("x", CYAN));
|
||||
}
|
||||
if buttons.y() {
|
||||
icons.push_front(("y", CYAN));
|
||||
}
|
||||
if buttons.l() || buttons.real_digital_l() {
|
||||
icons.push_front(("lb", BLUE));
|
||||
}
|
||||
if buttons.r() || buttons.real_digital_r() {
|
||||
icons.push_front(("rb", BLUE));
|
||||
}
|
||||
if buttons.zl() {
|
||||
icons.push_front(("zl", PURPLE));
|
||||
}
|
||||
if buttons.zr() {
|
||||
icons.push_front(("zr", PURPLE));
|
||||
}
|
||||
if buttons.plus() {
|
||||
icons.push_front(("plus", WHITE));
|
||||
}
|
||||
if buttons.minus() {
|
||||
icons.push_front(("minus", WHITE));
|
||||
}
|
||||
if buttons.dpad_up() {
|
||||
icons.push_front(("dpad_up", WHITE));
|
||||
}
|
||||
if buttons.dpad_down() {
|
||||
icons.push_front(("dpad_down", WHITE));
|
||||
}
|
||||
if buttons.dpad_left() {
|
||||
icons.push_front(("dpad_left", WHITE));
|
||||
}
|
||||
if buttons.dpad_right() {
|
||||
icons.push_front(("dpad_right", WHITE));
|
||||
}
|
||||
|
||||
icons
|
||||
}
|
||||
|
||||
fn is_smash_different(&self, other: &InputLog) -> bool {
|
||||
self.smash_inputs.buttons != other.smash_inputs.buttons
|
||||
|| self.smash_binned_lstick() != other.smash_binned_lstick()
|
||||
|| self.smash_binned_rstick() != other.smash_binned_rstick()
|
||||
|| (unsafe { MENU.input_display_status.as_bool() } && self.status != other.status)
|
||||
}
|
||||
|
||||
fn smash_binned_lstick(&self) -> (DirectionStrength, f32) {
|
||||
bin_stick_values(self.smash_inputs.lstick_x, self.smash_inputs.lstick_y)
|
||||
}
|
||||
|
||||
fn smash_binned_rstick(&self) -> (DirectionStrength, f32) {
|
||||
bin_stick_values(self.smash_inputs.rstick_x, self.smash_inputs.rstick_y)
|
||||
}
|
||||
|
||||
fn is_raw_different(&self, other: &InputLog) -> bool {
|
||||
self.raw_inputs.current_buttons != other.raw_inputs.current_buttons
|
||||
|| self.raw_binned_lstick() != other.raw_binned_lstick()
|
||||
|| self.raw_binned_rstick() != other.raw_binned_rstick()
|
||||
|| (unsafe { MENU.input_display_status.as_bool() } && self.status != other.status)
|
||||
}
|
||||
|
||||
fn raw_binned_lstick(&self) -> (DirectionStrength, f32) {
|
||||
let x = (self.raw_inputs.left_stick_x / STICK_CLAMP_MULTIPLIER) as i8;
|
||||
let y = (self.raw_inputs.left_stick_y / STICK_CLAMP_MULTIPLIER) as i8;
|
||||
bin_stick_values(x, y)
|
||||
}
|
||||
|
||||
fn raw_binned_rstick(&self) -> (DirectionStrength, f32) {
|
||||
let x = (self.raw_inputs.right_stick_x / STICK_CLAMP_MULTIPLIER) as i8;
|
||||
let y = (self.raw_inputs.right_stick_y / STICK_CLAMP_MULTIPLIER) as i8;
|
||||
bin_stick_values(x, y)
|
||||
}
|
||||
}
|
||||
|
||||
fn insert_in_place<T>(array: &mut [T], value: T, index: usize) {
|
||||
array[index..].rotate_right(1);
|
||||
array[index] = value;
|
||||
}
|
||||
|
||||
fn insert_in_front<T>(array: &mut [T], value: T) {
|
||||
insert_in_place(array, value, 0);
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
pub static ref P1_INPUT_LOGS: Mutex<[InputLog; NUM_LOGS]> =
|
||||
Mutex::new([InputLog::default(); NUM_LOGS]);
|
||||
}
|
||||
|
||||
pub fn handle_final_input_mapping(
|
||||
player_idx: i32,
|
||||
controller_struct: &SomeControllerStruct,
|
||||
out: *mut MappedInputs,
|
||||
) {
|
||||
unsafe {
|
||||
if MENU.input_display == InputDisplay::NONE {
|
||||
return;
|
||||
}
|
||||
|
||||
if QUICK_MENU_ACTIVE {
|
||||
return;
|
||||
}
|
||||
|
||||
if player_idx == 0 {
|
||||
let module_accessor = try_get_module_accessor(FighterId::Player);
|
||||
if module_accessor.is_none() {
|
||||
return;
|
||||
}
|
||||
let module_accessor = module_accessor.unwrap();
|
||||
|
||||
let current_frame = frame_counter::get_frame_count(*PER_LOG_FRAME_COUNTER);
|
||||
let current_overall_frame = frame_counter::get_frame_count(*OVERALL_FRAME_COUNTER);
|
||||
// We should always be counting
|
||||
frame_counter::start_counting(*PER_LOG_FRAME_COUNTER);
|
||||
frame_counter::start_counting(*OVERALL_FRAME_COUNTER);
|
||||
|
||||
let potential_input_log = InputLog {
|
||||
ttl: 600,
|
||||
frames: 1,
|
||||
overall_frame: current_overall_frame,
|
||||
raw_inputs: *controller_struct.controller,
|
||||
smash_inputs: *out,
|
||||
status: StatusModule::status_kind(module_accessor),
|
||||
fighter_kind: utility::get_kind(&mut *module_accessor),
|
||||
};
|
||||
|
||||
let input_logs = &mut *P1_INPUT_LOGS.lock();
|
||||
let latest_input_log = input_logs.first_mut().unwrap();
|
||||
let prev_overall_frames = latest_input_log.overall_frame;
|
||||
let prev_ttl = latest_input_log.ttl;
|
||||
// Only update if we are on a new frame according to the latest log
|
||||
let is_new_frame = prev_overall_frames != current_overall_frame;
|
||||
if is_new_frame && latest_input_log.is_different(&potential_input_log) {
|
||||
frame_counter::reset_frame_count(*PER_LOG_FRAME_COUNTER);
|
||||
// We should count this frame already
|
||||
frame_counter::tick_idx(*PER_LOG_FRAME_COUNTER);
|
||||
insert_in_front(input_logs, potential_input_log);
|
||||
let draw_log_base_idx = &mut *DRAW_LOG_BASE_IDX.data_ptr();
|
||||
*draw_log_base_idx = (*draw_log_base_idx + 1) % NUM_LOGS;
|
||||
} else if is_new_frame {
|
||||
*latest_input_log = potential_input_log;
|
||||
latest_input_log.frames = std::cmp::min(current_frame, 99);
|
||||
latest_input_log.ttl = prev_ttl;
|
||||
}
|
||||
|
||||
// Decrease TTL
|
||||
for input_log in input_logs.iter_mut() {
|
||||
if input_log.ttl > 0 && is_new_frame {
|
||||
input_log.ttl -= 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@ use crate::common::{
|
|||
};
|
||||
use crate::training::mash;
|
||||
use crate::training::ui::notifications::{clear_notifications, color_notification};
|
||||
use crate::{error, warn};
|
||||
|
||||
#[derive(PartialEq, Debug)]
|
||||
pub enum InputRecordState {
|
||||
|
@ -102,17 +103,17 @@ unsafe fn should_mash_playback() {
|
|||
|
||||
if is_in_hitstun(&mut *cpu_module_accessor) {
|
||||
// if we're in hitstun and want to enter the frame we start hitstop for SDI, start if we're in any damage status instantly
|
||||
if MENU.hitstun_playback == HitstunPlayback::Instant {
|
||||
if MENU.hitstun_playback == HitstunPlayback::INSTANT {
|
||||
should_playback = true;
|
||||
}
|
||||
// if we want to wait until we exit hitstop and begin flying away for shield art etc, start if we're not in hitstop
|
||||
if MENU.hitstun_playback == HitstunPlayback::Hitstop
|
||||
if MENU.hitstun_playback == HitstunPlayback::HITSTOP
|
||||
&& !StopModule::is_stop(cpu_module_accessor)
|
||||
{
|
||||
should_playback = true;
|
||||
}
|
||||
// if we're in hitstun and want to wait till FAF to act, then we want to match our starting status to the correct transition term to see if we can hitstun cancel
|
||||
if MENU.hitstun_playback == HitstunPlayback::Hitstun && can_transition(cpu_module_accessor)
|
||||
if MENU.hitstun_playback == HitstunPlayback::HITSTUN && can_transition(cpu_module_accessor)
|
||||
{
|
||||
should_playback = true;
|
||||
}
|
||||
|
@ -191,12 +192,12 @@ unsafe fn handle_recording_for_fighter(module_accessor: &mut BattleObjectModuleA
|
|||
let fighter_kind = utility::get_kind(module_accessor);
|
||||
let fighter_is_nana = fighter_kind == *FIGHTER_KIND_NANA;
|
||||
|
||||
CURRENT_RECORD_SLOT = MENU.recording_slot.into_idx();
|
||||
CURRENT_RECORD_SLOT = MENU.recording_slot.into_idx().unwrap_or(0);
|
||||
|
||||
if entry_id_int == 0 && !fighter_is_nana {
|
||||
if button_config::combo_passes(button_config::ButtonCombo::InputPlayback) {
|
||||
playback(MENU.playback_button_slots.get_random().into_idx());
|
||||
} else if MENU.record_trigger.contains(RecordTrigger::COMMAND)
|
||||
} else if MENU.record_trigger.contains(&RecordTrigger::COMMAND)
|
||||
&& button_config::combo_passes(button_config::ButtonCombo::InputRecord)
|
||||
{
|
||||
lockout_record();
|
||||
|
@ -215,7 +216,7 @@ unsafe fn handle_recording_for_fighter(module_accessor: &mut BattleObjectModuleA
|
|||
|
||||
// If we need to crop the recording for neutral input
|
||||
// INPUT_RECORD_FRAME must be > 0 to prevent bounding errors
|
||||
if INPUT_RECORD == Record && MENU.recording_crop == OnOff::On && INPUT_RECORD_FRAME > 0
|
||||
if INPUT_RECORD == Record && MENU.recording_crop == OnOff::ON && INPUT_RECORD_FRAME > 0
|
||||
{
|
||||
while INPUT_RECORD_FRAME > 0 && is_input_neutral(INPUT_RECORD_FRAME - 1) {
|
||||
// Discard frames at the end of the recording until the last frame with input
|
||||
|
@ -227,7 +228,7 @@ unsafe fn handle_recording_for_fighter(module_accessor: &mut BattleObjectModuleA
|
|||
|
||||
INPUT_RECORD_FRAME = 0;
|
||||
|
||||
if MENU.playback_loop == OnOff::On && INPUT_RECORD == Playback {
|
||||
if MENU.playback_loop == OnOff::ON && INPUT_RECORD == Playback {
|
||||
playback(Some(CURRENT_PLAYBACK_SLOT));
|
||||
} else {
|
||||
INPUT_RECORD = None;
|
||||
|
@ -338,11 +339,11 @@ pub unsafe fn lockout_record() {
|
|||
// Returns whether we did playback
|
||||
pub unsafe fn playback(slot: Option<usize>) -> bool {
|
||||
if INPUT_RECORD == Pause {
|
||||
println!("Tried to playback during lockout!");
|
||||
warn!("Tried to playback during lockout!");
|
||||
return false;
|
||||
}
|
||||
if slot.is_none() {
|
||||
println!("Tried to playback without a slot selected!");
|
||||
warn!("Tried to playback without a slot selected!");
|
||||
return false;
|
||||
}
|
||||
let slot = slot.unwrap();
|
||||
|
@ -425,8 +426,6 @@ pub unsafe fn handle_final_input_mapping(player_idx: i32, out: *mut MappedInputs
|
|||
|
||||
P1_FINAL_MAPPING.lock()[CURRENT_RECORD_SLOT][INPUT_RECORD_FRAME] = *out;
|
||||
*out = MappedInputs::empty(); // don't control player while recording
|
||||
|
||||
//println!("Stored Player Input! Frame: {}", INPUT_RECORD_FRAME);
|
||||
}
|
||||
// Don't allow for player input during Lockout
|
||||
if POSSESSION == Lockout {
|
||||
|
@ -468,7 +467,7 @@ unsafe fn set_cpu_controls(p_data: *mut *mut u8) {
|
|||
INPUT_RECORD = Record;
|
||||
POSSESSION = Standby;
|
||||
}
|
||||
Ordering::Less => println!("LOCKOUT_FRAME OUT OF BOUNDS"),
|
||||
Ordering::Less => error!("LOCKOUT_FRAME OUT OF BOUNDS"),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -504,8 +503,6 @@ unsafe fn set_cpu_controls(p_data: *mut *mut u8) {
|
|||
);
|
||||
}
|
||||
|
||||
//println!("Overriding Cpu Player: {}, Frame: {}, BUFFER_FRAME: {}, STARTING_STATUS: {}, INPUT_RECORD: {:#?}, POSSESSION: {:#?}", controller_no, INPUT_RECORD_FRAME, BUFFER_FRAME, STARTING_STATUS, INPUT_RECORD, POSSESSION);
|
||||
|
||||
let mut saved_mapped_inputs = P1_FINAL_MAPPING.lock()[if INPUT_RECORD == Record {
|
||||
CURRENT_RECORD_SLOT
|
||||
} else {
|
||||
|
@ -538,7 +535,6 @@ unsafe fn set_cpu_controls(p_data: *mut *mut u8) {
|
|||
};
|
||||
(*controller_data).clamped_lstick_x = clamped_lstick_x;
|
||||
(*controller_data).clamped_lstick_y = clamped_lstick_y;
|
||||
//println!("CPU Buttons: {:#018b}", (*controller_data).buttons);
|
||||
|
||||
// Keep counting frames, unless we're in standby waiting for an input, or are buffering an option
|
||||
// When buffering an option, we keep inputting the first frame of input during the buffer window
|
||||
|
|
|
@ -58,7 +58,7 @@ fn roll_ledge_case() {
|
|||
fn get_ledge_option() -> Option<Action> {
|
||||
unsafe {
|
||||
let mut override_action: Option<Action> = None;
|
||||
let regular_action = if MENU.mash_triggers.contains(MashTrigger::LEDGE) {
|
||||
let regular_action = if MENU.mash_triggers.contains(&MashTrigger::LEDGE) {
|
||||
Some(MENU.mash_state.get_random())
|
||||
} else {
|
||||
None
|
||||
|
|
|
@ -80,14 +80,13 @@ pub fn buffer_action(action: Action) {
|
|||
unsafe {
|
||||
// exit playback if we want to perform mash actions out of it
|
||||
// TODO: Figure out some way to deal with trying to playback into another playback
|
||||
if MENU.playback_mash == OnOff::On
|
||||
if MENU.playback_mash == OnOff::ON
|
||||
&& input_record::is_playback()
|
||||
&& !input_record::is_recording()
|
||||
&& !input_record::is_standby()
|
||||
&& !is_playback_queued()
|
||||
&& !action.is_playback()
|
||||
{
|
||||
//println!("Stopping mash playback for menu option!");
|
||||
// if we don't want to leave playback on mash actions, then don't perform the mash
|
||||
if input_record::is_playback() {
|
||||
input_record::stop_playback();
|
||||
|
@ -221,7 +220,7 @@ unsafe fn get_buffered_action(
|
|||
let action = MENU.tech_action_override.get_random();
|
||||
if action != Action::empty() {
|
||||
Some(action)
|
||||
} else if MENU.mash_triggers.contains(MashTrigger::TECH) {
|
||||
} else if MENU.mash_triggers.contains(&MashTrigger::TECH) {
|
||||
Some(MENU.mash_state.get_random())
|
||||
} else {
|
||||
None
|
||||
|
@ -230,7 +229,7 @@ unsafe fn get_buffered_action(
|
|||
let action = MENU.clatter_override.get_random();
|
||||
if action != Action::empty() {
|
||||
Some(action)
|
||||
} else if MENU.mash_triggers.contains(MashTrigger::CLATTER) {
|
||||
} else if MENU.mash_triggers.contains(&MashTrigger::CLATTER) {
|
||||
Some(MENU.mash_state.get_random())
|
||||
} else {
|
||||
None
|
||||
|
@ -241,7 +240,7 @@ unsafe fn get_buffered_action(
|
|||
let action = MENU.tumble_override.get_random();
|
||||
if action != Action::empty() {
|
||||
Some(action)
|
||||
} else if MENU.mash_triggers.contains(MashTrigger::TUMBLE) {
|
||||
} else if MENU.mash_triggers.contains(&MashTrigger::TUMBLE) {
|
||||
Some(MENU.mash_state.get_random())
|
||||
} else {
|
||||
None
|
||||
|
@ -250,7 +249,7 @@ unsafe fn get_buffered_action(
|
|||
let action = MENU.hitstun_override.get_random();
|
||||
if action != Action::empty() {
|
||||
Some(action)
|
||||
} else if MENU.mash_triggers.contains(MashTrigger::HIT) {
|
||||
} else if MENU.mash_triggers.contains(&MashTrigger::HIT) {
|
||||
Some(MENU.mash_state.get_random())
|
||||
} else {
|
||||
None
|
||||
|
@ -259,7 +258,7 @@ unsafe fn get_buffered_action(
|
|||
let action = MENU.parry_override.get_random();
|
||||
if action != Action::empty() {
|
||||
Some(action)
|
||||
} else if MENU.mash_triggers.contains(MashTrigger::PARRY) {
|
||||
} else if MENU.mash_triggers.contains(&MashTrigger::PARRY) {
|
||||
Some(MENU.mash_state.get_random())
|
||||
} else {
|
||||
None
|
||||
|
@ -268,7 +267,7 @@ unsafe fn get_buffered_action(
|
|||
let action = MENU.footstool_override.get_random();
|
||||
if action != Action::empty() {
|
||||
Some(action)
|
||||
} else if MENU.mash_triggers.contains(MashTrigger::FOOTSTOOL) {
|
||||
} else if MENU.mash_triggers.contains(&MashTrigger::FOOTSTOOL) {
|
||||
Some(MENU.mash_state.get_random())
|
||||
} else {
|
||||
None
|
||||
|
@ -277,7 +276,7 @@ unsafe fn get_buffered_action(
|
|||
let action = MENU.trump_override.get_random();
|
||||
if action != Action::empty() {
|
||||
Some(action)
|
||||
} else if MENU.mash_triggers.contains(MashTrigger::TRUMP) {
|
||||
} else if MENU.mash_triggers.contains(&MashTrigger::TRUMP) {
|
||||
Some(MENU.mash_state.get_random())
|
||||
} else {
|
||||
None
|
||||
|
@ -286,20 +285,20 @@ unsafe fn get_buffered_action(
|
|||
let action = MENU.landing_override.get_random();
|
||||
if action != Action::empty() {
|
||||
Some(action)
|
||||
} else if MENU.mash_triggers.contains(MashTrigger::LANDING) {
|
||||
} else if MENU.mash_triggers.contains(&MashTrigger::LANDING) {
|
||||
Some(MENU.mash_state.get_random())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else if (MENU.mash_triggers.contains(MashTrigger::GROUNDED) && is_grounded(module_accessor))
|
||||
|| (MENU.mash_triggers.contains(MashTrigger::AIRBORNE) && is_airborne(module_accessor))
|
||||
|| (MENU.mash_triggers.contains(MashTrigger::DISTANCE_CLOSE)
|
||||
} else if (MENU.mash_triggers.contains(&MashTrigger::GROUNDED) && is_grounded(module_accessor))
|
||||
|| (MENU.mash_triggers.contains(&MashTrigger::AIRBORNE) && is_airborne(module_accessor))
|
||||
|| (MENU.mash_triggers.contains(&MashTrigger::DISTANCE_CLOSE)
|
||||
&& fighter_distance < DISTANCE_CLOSE_THRESHOLD)
|
||||
|| (MENU.mash_triggers.contains(MashTrigger::DISTANCE_MID)
|
||||
|| (MENU.mash_triggers.contains(&MashTrigger::DISTANCE_MID)
|
||||
&& fighter_distance < DISTANCE_MID_THRESHOLD)
|
||||
|| (MENU.mash_triggers.contains(MashTrigger::DISTANCE_FAR)
|
||||
|| (MENU.mash_triggers.contains(&MashTrigger::DISTANCE_FAR)
|
||||
&& fighter_distance < DISTANCE_FAR_THRESHOLD)
|
||||
|| MENU.mash_triggers.contains(MashTrigger::ALWAYS)
|
||||
|| MENU.mash_triggers.contains(&MashTrigger::ALWAYS)
|
||||
{
|
||||
Some(MENU.mash_state.get_random())
|
||||
} else {
|
||||
|
|
|
@ -770,7 +770,7 @@ pub unsafe fn handle_reused_ui(
|
|||
// If Little Mac is in the game and we're buffing him, set the meter to 100
|
||||
if (player_fighter_kind == *FIGHTER_KIND_LITTLEMAC
|
||||
|| cpu_fighter_kind == *FIGHTER_KIND_LITTLEMAC)
|
||||
&& MENU.buff_state.to_vec().contains(&BuffOption::KO)
|
||||
&& MENU.buff_state.contains(&BuffOption::KO)
|
||||
{
|
||||
param_2 = 100;
|
||||
}
|
||||
|
|
|
@ -143,7 +143,10 @@ pub struct SaveStateSlots {
|
|||
const NUM_SAVE_STATE_SLOTS: usize = 5;
|
||||
// I actually had to do it this way, a simple load-from-file in main() caused crashes.
|
||||
lazy_static::lazy_static! {
|
||||
static ref SAVE_STATE_SLOTS : Mutex<SaveStateSlots> = Mutex::new(load_from_file());
|
||||
static ref SAVE_STATE_SLOTS : Mutex<SaveStateSlots> = Mutex::new({
|
||||
info!("Initialized lazy_static: SAVE_STATE_SLOTS");
|
||||
load_from_file()
|
||||
});
|
||||
}
|
||||
|
||||
pub fn load_from_file() -> SaveStateSlots {
|
||||
|
@ -233,7 +236,7 @@ unsafe fn get_slot() -> usize {
|
|||
if random_slot != SaveStateSlot::empty() {
|
||||
RANDOM_SLOT
|
||||
} else {
|
||||
MENU.save_state_slot.as_idx() as usize
|
||||
MENU.save_state_slot.into_idx().unwrap_or(0)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -253,9 +256,13 @@ pub unsafe fn is_loading() -> bool {
|
|||
|
||||
pub unsafe fn should_mirror() -> f32 {
|
||||
match MENU.save_state_mirroring {
|
||||
SaveStateMirroring::None => 1.0,
|
||||
SaveStateMirroring::Alternate => -1.0 * MIRROR_STATE,
|
||||
SaveStateMirroring::Random => ([-1.0, 1.0])[get_random_int(2) as usize],
|
||||
SaveStateMirroring::NONE => 1.0,
|
||||
SaveStateMirroring::ALTERNATE => -1.0 * MIRROR_STATE,
|
||||
SaveStateMirroring::RANDOM => ([-1.0, 1.0])[get_random_int(2) as usize],
|
||||
_ => panic!(
|
||||
"Invalid value in should_mirror: {}",
|
||||
MENU.save_state_mirroring
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -409,7 +416,7 @@ pub unsafe fn on_death(fighter_kind: i32, module_accessor: &mut app::BattleObjec
|
|||
}
|
||||
|
||||
pub unsafe fn save_states(module_accessor: &mut app::BattleObjectModuleAccessor) {
|
||||
if MENU.save_state_enable == OnOff::Off {
|
||||
if MENU.save_state_enable == OnOff::OFF {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -441,7 +448,7 @@ pub unsafe fn save_states(module_accessor: &mut app::BattleObjectModuleAccessor)
|
|||
.contains(&fighter_kind);
|
||||
|
||||
// Reset state
|
||||
let autoload_reset = MENU.save_state_autoload == OnOff::On
|
||||
let autoload_reset = MENU.save_state_autoload == OnOff::ON
|
||||
&& save_state.state == NoAction
|
||||
&& is_dead(module_accessor);
|
||||
let mut triggered_reset: bool = false;
|
||||
|
@ -452,7 +459,7 @@ pub unsafe fn save_states(module_accessor: &mut app::BattleObjectModuleAccessor)
|
|||
if save_state.state == NoAction {
|
||||
let random_slot = MENU.randomize_slots.get_random();
|
||||
let slot = if random_slot != SaveStateSlot::empty() {
|
||||
RANDOM_SLOT = random_slot.as_idx();
|
||||
RANDOM_SLOT = random_slot.into_idx().unwrap_or(0);
|
||||
RANDOM_SLOT
|
||||
} else {
|
||||
selected_slot
|
||||
|
@ -578,7 +585,10 @@ pub unsafe fn save_states(module_accessor: &mut app::BattleObjectModuleAccessor)
|
|||
set_damage(module_accessor, pct);
|
||||
}
|
||||
SaveDamage::DEFAULT => {}
|
||||
_ => {}
|
||||
_ => panic!(
|
||||
"Invalid value in save_states()::save_damage_player: {}",
|
||||
MENU.save_damage_player
|
||||
),
|
||||
}
|
||||
} else {
|
||||
match MENU.save_damage_cpu {
|
||||
|
@ -594,12 +604,15 @@ pub unsafe fn save_states(module_accessor: &mut app::BattleObjectModuleAccessor)
|
|||
set_damage(module_accessor, pct);
|
||||
}
|
||||
SaveDamage::DEFAULT => {}
|
||||
_ => {}
|
||||
_ => panic!(
|
||||
"Invalid value in save_states()::save_damage_cpu: {}",
|
||||
MENU.save_damage_cpu
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
// Set to held item
|
||||
if !is_cpu && !fighter_is_nana && MENU.character_item != CharacterItem::None {
|
||||
if !is_cpu && !fighter_is_nana && MENU.character_item != CharacterItem::NONE {
|
||||
apply_item(MENU.character_item);
|
||||
}
|
||||
|
||||
|
@ -648,7 +661,7 @@ pub unsafe fn save_states(module_accessor: &mut app::BattleObjectModuleAccessor)
|
|||
}
|
||||
|
||||
// if we're recording on state load, record
|
||||
if MENU.record_trigger.contains(RecordTrigger::SAVESTATE) {
|
||||
if MENU.record_trigger.contains(&RecordTrigger::SAVESTATE) {
|
||||
input_record::lockout_record();
|
||||
return;
|
||||
}
|
||||
|
@ -693,8 +706,8 @@ pub unsafe fn save_states(module_accessor: &mut app::BattleObjectModuleAccessor)
|
|||
if button_config::combo_passes(button_config::ButtonCombo::SaveState) {
|
||||
// Don't begin saving state if Nana's delayed input is captured
|
||||
MIRROR_STATE = 1.0;
|
||||
save_state_player(MENU.save_state_slot.as_idx() as usize).state = Save;
|
||||
save_state_cpu(MENU.save_state_slot.as_idx() as usize).state = Save;
|
||||
save_state_player(MENU.save_state_slot.into_idx().unwrap_or(0)).state = Save;
|
||||
save_state_cpu(MENU.save_state_slot.into_idx().unwrap_or(0)).state = Save;
|
||||
notifications::clear_notifications("Save State");
|
||||
notifications::notification(
|
||||
"Save State".to_string(),
|
||||
|
|
|
@ -107,7 +107,7 @@ pub unsafe fn get_param_float(
|
|||
return None;
|
||||
}
|
||||
|
||||
if MENU.shield_state != Shield::None {
|
||||
if MENU.shield_state != Shield::NONE {
|
||||
handle_oos_offset(module_accessor);
|
||||
}
|
||||
|
||||
|
@ -121,8 +121,8 @@ fn handle_shield_decay(param_type: u64, param_hash: u64) -> Option<f32> {
|
|||
menu_state = MENU.shield_state;
|
||||
}
|
||||
|
||||
if menu_state != Shield::Infinite
|
||||
&& menu_state != Shield::Constant
|
||||
if menu_state != Shield::INFINITE
|
||||
&& menu_state != Shield::CONSTANT
|
||||
&& !should_pause_shield_decay()
|
||||
{
|
||||
return None;
|
||||
|
@ -161,7 +161,7 @@ pub unsafe fn param_installer() {
|
|||
CACHED_SHIELD_DAMAGE_MUL = Some(common_params.shield_damage_mul);
|
||||
}
|
||||
|
||||
if is_training_mode() && (MENU.shield_state == Shield::Infinite) {
|
||||
if is_training_mode() && (MENU.shield_state == Shield::INFINITE) {
|
||||
// if you are in training mode and have infinite shield enabled,
|
||||
// set the game's shield_damage_mul to 0.0
|
||||
common_params.shield_damage_mul = 0.0;
|
||||
|
@ -192,7 +192,7 @@ pub fn should_hold_shield(module_accessor: &mut app::BattleObjectModuleAccessor)
|
|||
|
||||
// We should hold shield if the state requires it
|
||||
if unsafe { save_states::is_loading() }
|
||||
|| ![Shield::Hold, Shield::Infinite, Shield::Constant].contains(shield_state)
|
||||
|| ![Shield::HOLD, Shield::INFINITE, Shield::CONSTANT].contains(shield_state)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
@ -223,7 +223,7 @@ unsafe fn mod_handle_sub_guard_cont(fighter: &mut L2CFighterCommon) {
|
|||
}
|
||||
|
||||
// Enable shield decay
|
||||
if MENU.shield_state == Shield::Hold {
|
||||
if MENU.shield_state == Shield::HOLD {
|
||||
set_shield_decay(true);
|
||||
}
|
||||
|
||||
|
@ -245,7 +245,7 @@ unsafe fn mod_handle_sub_guard_cont(fighter: &mut L2CFighterCommon) {
|
|||
return;
|
||||
}
|
||||
|
||||
if MENU.mash_triggers.contains(MashTrigger::SHIELDSTUN) {
|
||||
if MENU.mash_triggers.contains(&MashTrigger::SHIELDSTUN) {
|
||||
if MENU.shieldstun_override == Action::empty() {
|
||||
mash::external_buffer_menu_mash(MENU.mash_state.get_random())
|
||||
} else {
|
||||
|
@ -360,7 +360,7 @@ fn needs_oos_handling_drop_shield() -> bool {
|
|||
shield_state = &MENU.shield_state;
|
||||
}
|
||||
// If we're supposed to be holding shield, let airdodge make us drop shield
|
||||
if [Shield::Hold, Shield::Infinite, Shield::Constant].contains(shield_state) {
|
||||
if [Shield::HOLD, Shield::INFINITE, Shield::CONSTANT].contains(shield_state) {
|
||||
suspend_shield(Action::AIR_DODGE);
|
||||
}
|
||||
return true;
|
||||
|
@ -373,7 +373,7 @@ fn needs_oos_handling_drop_shield() -> bool {
|
|||
shield_state = &MENU.shield_state;
|
||||
}
|
||||
// If we're supposed to be holding shield, let airdodge make us drop shield
|
||||
if [Shield::Hold, Shield::Infinite, Shield::Constant].contains(shield_state) {
|
||||
if [Shield::HOLD, Shield::INFINITE, Shield::CONSTANT].contains(shield_state) {
|
||||
suspend_shield(Action::AIR_DODGE);
|
||||
}
|
||||
return true;
|
||||
|
@ -385,7 +385,7 @@ fn needs_oos_handling_drop_shield() -> bool {
|
|||
shield_state = &MENU.shield_state;
|
||||
}
|
||||
// Don't drop shield on shield hit if we're supposed to be holding shield
|
||||
if [Shield::Hold, Shield::Infinite, Shield::Constant].contains(shield_state) {
|
||||
if [Shield::HOLD, Shield::INFINITE, Shield::CONSTANT].contains(shield_state) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
|
|
|
@ -123,7 +123,7 @@ unsafe fn handle_grnd_tech(
|
|||
}
|
||||
_ => false,
|
||||
};
|
||||
if do_tech && MENU.mash_triggers.contains(MashTrigger::TECH) {
|
||||
if do_tech && MENU.mash_triggers.contains(&MashTrigger::TECH) {
|
||||
if MENU.tech_action_override == Action::empty() {
|
||||
mash::external_buffer_menu_mash(MENU.mash_state.get_random())
|
||||
} else {
|
||||
|
@ -170,7 +170,7 @@ unsafe fn handle_wall_tech(
|
|||
}
|
||||
_ => false,
|
||||
};
|
||||
if do_tech && MENU.mash_triggers.contains(MashTrigger::TECH) {
|
||||
if do_tech && MENU.mash_triggers.contains(&MashTrigger::TECH) {
|
||||
if MENU.tech_action_override == Action::empty() {
|
||||
mash::external_buffer_menu_mash(MENU.mash_state.get_random())
|
||||
} else {
|
||||
|
@ -205,7 +205,7 @@ unsafe fn handle_ceil_tech(
|
|||
|
||||
*status_kind = FIGHTER_STATUS_KIND_PASSIVE_CEIL.as_lua_int();
|
||||
*unk = LUA_TRUE;
|
||||
if MENU.mash_triggers.contains(MashTrigger::TECH) {
|
||||
if MENU.mash_triggers.contains(&MashTrigger::TECH) {
|
||||
if MENU.tech_action_override == Action::empty() {
|
||||
mash::external_buffer_menu_mash(MENU.mash_state.get_random())
|
||||
} else {
|
||||
|
@ -277,7 +277,7 @@ pub unsafe fn get_command_flag_cat(module_accessor: &mut BattleObjectModuleAcces
|
|||
|
||||
if requested_status != 0 {
|
||||
StatusModule::change_status_force(module_accessor, requested_status, true);
|
||||
if MENU.mash_triggers.contains(MashTrigger::MISTECH) {
|
||||
if MENU.mash_triggers.contains(&MashTrigger::MISTECH) {
|
||||
if MENU.tech_action_override == Action::empty() {
|
||||
mash::external_buffer_menu_mash(MENU.mash_state.get_random())
|
||||
} else {
|
||||
|
@ -347,7 +347,7 @@ unsafe fn get_snake_laydown_lockout_time(module_accessor: &mut BattleObjectModul
|
|||
}
|
||||
|
||||
pub unsafe fn hide_tech() {
|
||||
if !is_training_mode() || MENU.tech_hide == OnOff::Off {
|
||||
if !is_training_mode() || MENU.tech_hide == OnOff::OFF {
|
||||
return;
|
||||
}
|
||||
let module_accessor = get_module_accessor(FighterId::CPU);
|
||||
|
@ -409,7 +409,7 @@ pub unsafe fn handle_fighter_req_quake_pos(
|
|||
return original!()(camera_module, quake_kind);
|
||||
}
|
||||
let status = StatusModule::status_kind(module_accessor);
|
||||
if status == FIGHTER_STATUS_KIND_DOWN && MENU.tech_hide == OnOff::On {
|
||||
if status == FIGHTER_STATUS_KIND_DOWN && MENU.tech_hide == OnOff::ON {
|
||||
// We're hiding techs, prevent mistech quake from giving away missed tech
|
||||
return original!()(camera_module, *CAMERA_QUAKE_KIND_NONE);
|
||||
}
|
||||
|
@ -452,7 +452,7 @@ pub struct CameraManager {
|
|||
|
||||
unsafe fn set_fixed_camera_values() {
|
||||
let camera_manager = get_camera_manager();
|
||||
if MENU.tech_hide == OnOff::Off {
|
||||
if MENU.tech_hide == OnOff::OFF {
|
||||
// Use Stage's Default Values for fixed Camera
|
||||
camera_manager.fixed_camera_center = DEFAULT_FIXED_CAM_CENTER;
|
||||
} else {
|
||||
|
|
|
@ -193,9 +193,9 @@ pub unsafe fn draw(root_pane: &Pane) {
|
|||
.find_pane_by_name_recursive("TrModInputLog")
|
||||
.unwrap();
|
||||
logs_pane.set_visible(
|
||||
!QUICK_MENU_ACTIVE && !VANILLA_MENU_ACTIVE && MENU.input_display != InputDisplay::None,
|
||||
!QUICK_MENU_ACTIVE && !VANILLA_MENU_ACTIVE && MENU.input_display != InputDisplay::NONE,
|
||||
);
|
||||
if MENU.input_display == InputDisplay::None {
|
||||
if MENU.input_display == InputDisplay::NONE {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -3,19 +3,18 @@ use std::collections::HashMap;
|
|||
use lazy_static::lazy_static;
|
||||
use skyline::nn::ui2d::*;
|
||||
use smash::ui2d::{SmashPane, SmashTextBox};
|
||||
use training_mod_tui::gauge::GaugeState;
|
||||
use training_mod_tui::{App, AppPage, NUM_LISTS};
|
||||
use training_mod_tui::{
|
||||
App, AppPage, ConfirmationState, SliderState, NX_SUBMENU_COLUMNS, NX_SUBMENU_ROWS,
|
||||
};
|
||||
|
||||
use crate::common::menu::{MENU_CLOSE_FRAME_COUNTER, MENU_CLOSE_WAIT_FRAMES, MENU_RECEIVED_INPUT};
|
||||
use crate::training::frame_counter;
|
||||
use crate::{common, common::menu::QUICK_MENU_ACTIVE, input::*};
|
||||
use training_mod_consts::TOGGLE_MAX;
|
||||
|
||||
use super::fade_out;
|
||||
use super::set_icon_text;
|
||||
|
||||
pub static NUM_MENU_TEXT_OPTIONS: usize = 32;
|
||||
pub static _NUM_MENU_TABS: usize = 3;
|
||||
|
||||
const BG_LEFT_ON_WHITE_COLOR: ResColor = ResColor {
|
||||
r: 0,
|
||||
g: 28,
|
||||
|
@ -78,171 +77,188 @@ lazy_static! {
|
|||
]);
|
||||
}
|
||||
|
||||
unsafe fn render_submenu_page(app: &App, root_pane: &Pane) {
|
||||
let tab_selected = app.tab_selected();
|
||||
let tab = app.menu_items.get(tab_selected).unwrap();
|
||||
let submenu_ids = app.submenu_ids();
|
||||
|
||||
(0..NUM_MENU_TEXT_OPTIONS)
|
||||
// Valid options in this submenu
|
||||
.filter_map(|idx| tab.idx_to_list_idx_opt(idx))
|
||||
.for_each(|(list_section, list_idx)| {
|
||||
let menu_button_row = root_pane
|
||||
.find_pane_by_name_recursive(format!("TrModMenuButtonRow{list_idx}").as_str())
|
||||
.unwrap();
|
||||
menu_button_row.set_visible(true);
|
||||
|
||||
let menu_button = menu_button_row
|
||||
.find_pane_by_name_recursive(format!("Button{list_section}").as_str())
|
||||
.unwrap();
|
||||
|
||||
let title_text = menu_button
|
||||
.find_pane_by_name_recursive("TitleTxt")
|
||||
.unwrap()
|
||||
.as_textbox();
|
||||
|
||||
let title_bg = menu_button
|
||||
.find_pane_by_name_recursive("TitleBg")
|
||||
.unwrap()
|
||||
.as_picture();
|
||||
|
||||
let title_bg_material = &mut *title_bg.material;
|
||||
|
||||
let list = &tab.lists[list_section];
|
||||
let submenu = &list.items[list_idx];
|
||||
let is_selected = list.state.selected().filter(|s| *s == list_idx).is_some();
|
||||
|
||||
title_text.set_text_string(submenu.submenu_title.as_str());
|
||||
|
||||
// In the actual 'layout.arc' file, every icon image is stacked
|
||||
// into a single container pane, with each image directly on top of another.
|
||||
// Hide all icon images, and strategically mark the icon that
|
||||
// corresponds with a particular button to be visible.
|
||||
submenu_ids.iter().for_each(|id| {
|
||||
let pane = menu_button.find_pane_by_name_recursive(id);
|
||||
if let Some(p) = pane {
|
||||
p.set_visible(id == &submenu.submenu_id);
|
||||
}
|
||||
});
|
||||
|
||||
menu_button
|
||||
.find_pane_by_name_recursive("check")
|
||||
.unwrap()
|
||||
.set_visible(false);
|
||||
|
||||
if is_selected {
|
||||
root_pane
|
||||
.find_pane_by_name_recursive("FooterTxt")
|
||||
.unwrap()
|
||||
.as_textbox()
|
||||
.set_text_string(submenu.help_text.as_str());
|
||||
|
||||
title_bg_material.set_white_res_color(BG_LEFT_ON_WHITE_COLOR);
|
||||
title_bg_material.set_black_res_color(BG_LEFT_ON_BLACK_COLOR);
|
||||
|
||||
title_text.text_shadow_enable(true);
|
||||
title_text.text_outline_enable(true);
|
||||
|
||||
title_text.set_color(255, 255, 255, 255);
|
||||
} else {
|
||||
title_bg_material.set_white_res_color(BG_LEFT_OFF_WHITE_COLOR);
|
||||
title_bg_material.set_black_res_color(BG_LEFT_OFF_BLACK_COLOR);
|
||||
|
||||
title_text.text_shadow_enable(false);
|
||||
title_text.text_outline_enable(false);
|
||||
|
||||
title_text.set_color(178, 199, 211, 255);
|
||||
}
|
||||
|
||||
menu_button.set_visible(true);
|
||||
menu_button
|
||||
.find_pane_by_name_recursive("Icon")
|
||||
.unwrap()
|
||||
.set_visible(true);
|
||||
});
|
||||
}
|
||||
|
||||
unsafe fn render_toggle_page(app: &App, root_pane: &Pane) {
|
||||
let (_title, _help_text, mut sub_menu_str_lists) = app.sub_menu_strs_and_states();
|
||||
(0..sub_menu_str_lists.len()).for_each(|list_section| {
|
||||
let sub_menu_str = sub_menu_str_lists[list_section].0.clone();
|
||||
let sub_menu_state = &mut sub_menu_str_lists[list_section].1;
|
||||
sub_menu_str
|
||||
.iter()
|
||||
.enumerate()
|
||||
.for_each(|(list_idx, (checked, name))| {
|
||||
let menu_button_row = root_pane
|
||||
.find_pane_by_name_recursive(format!("TrModMenuButtonRow{list_idx}").as_str())
|
||||
.unwrap();
|
||||
menu_button_row.set_visible(true);
|
||||
|
||||
unsafe fn render_submenu_page(app: &mut App, root_pane: &Pane) {
|
||||
let tabs_clone = app.tabs.clone(); // Need this to avoid double-borrow later on
|
||||
let tab = app.selected_tab();
|
||||
for row in 0..NX_SUBMENU_ROWS {
|
||||
let menu_button_row = root_pane
|
||||
.find_pane_by_name_recursive(format!("TrModMenuButtonRow{row}").as_str())
|
||||
.unwrap();
|
||||
menu_button_row.set_visible(true);
|
||||
for col in 0..NX_SUBMENU_COLUMNS {
|
||||
if let Some(submenu) = tab.submenus.get(row, col) {
|
||||
// Find all the panes we need to modify
|
||||
let menu_button = menu_button_row
|
||||
.find_pane_by_name_recursive(format!("Button{list_section}").as_str())
|
||||
.find_pane_by_name_recursive(format!("Button{col}").as_str())
|
||||
.unwrap();
|
||||
menu_button.set_visible(true);
|
||||
|
||||
let title_text = menu_button
|
||||
.find_pane_by_name_recursive("TitleTxt")
|
||||
.unwrap()
|
||||
.as_textbox();
|
||||
|
||||
let title_bg = menu_button
|
||||
.find_pane_by_name_recursive("TitleBg")
|
||||
.unwrap()
|
||||
.as_picture();
|
||||
let title_bg_material = &mut *title_bg.material;
|
||||
let is_selected = row == tab.submenus.state.selected_row().unwrap()
|
||||
&& col == tab.submenus.state.selected_col().unwrap();
|
||||
|
||||
let is_selected = sub_menu_state
|
||||
.selected()
|
||||
.filter(|s| *s == list_idx)
|
||||
.is_some();
|
||||
// Set Pane Visibility
|
||||
title_text.set_text_string(submenu.title);
|
||||
|
||||
let submenu_ids = app.submenu_ids();
|
||||
// In the actual 'layout.arc' file, every icon image is stacked
|
||||
// into a single container pane, with each image directly on top of another.
|
||||
// Hide all icon images, and strategically mark the icon that
|
||||
// corresponds with a particular button to be visible.
|
||||
|
||||
submenu_ids.iter().for_each(|id| {
|
||||
let pane = menu_button.find_pane_by_name_recursive(id);
|
||||
if let Some(p) = pane {
|
||||
p.set_visible(false);
|
||||
for t in tabs_clone.iter() {
|
||||
for s in t.submenus.iter() {
|
||||
let pane = menu_button.find_pane_by_name_recursive(s.id);
|
||||
if let Some(p) = pane {
|
||||
p.set_visible(s.id == submenu.id);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
title_text.set_text_string(name);
|
||||
menu_button
|
||||
.find_pane_by_name_recursive("check")
|
||||
.unwrap()
|
||||
.set_visible(true);
|
||||
.set_visible(false);
|
||||
|
||||
for value in 1..=TOGGLE_MAX {
|
||||
if let Some(pane) =
|
||||
menu_button.find_pane_by_name_recursive(format!("{}", value).as_str())
|
||||
{
|
||||
pane.set_visible(false);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if is_selected {
|
||||
// Help text
|
||||
root_pane
|
||||
.find_pane_by_name_recursive("FooterTxt")
|
||||
.unwrap()
|
||||
.as_textbox()
|
||||
.set_text_string(submenu.help_text);
|
||||
|
||||
title_bg_material.set_white_res_color(BG_LEFT_ON_WHITE_COLOR);
|
||||
title_bg_material.set_black_res_color(BG_LEFT_ON_BLACK_COLOR);
|
||||
title_text.text_shadow_enable(true);
|
||||
title_text.text_outline_enable(true);
|
||||
title_text.set_color(255, 255, 255, 255);
|
||||
} else {
|
||||
title_bg_material.set_white_res_color(BG_LEFT_OFF_WHITE_COLOR);
|
||||
title_bg_material.set_black_res_color(BG_LEFT_OFF_BLACK_COLOR);
|
||||
title_text.text_shadow_enable(false);
|
||||
title_text.text_outline_enable(false);
|
||||
title_text.set_color(178, 199, 211, 255);
|
||||
}
|
||||
menu_button.set_visible(true);
|
||||
menu_button
|
||||
.find_pane_by_name_recursive("Icon")
|
||||
.unwrap()
|
||||
.set_visible(*checked);
|
||||
.set_visible(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn render_toggle_page(app: &mut App, root_pane: &Pane) {
|
||||
let tabs_clone = app.tabs.clone(); // Need this to avoid double-borrow later on
|
||||
let submenu = app.selected_submenu();
|
||||
// If the options can only be toggled on or off, then use the check icon
|
||||
// instead of the number icons
|
||||
let use_check_icon = submenu.toggles.get(0, 0).unwrap().max == 1;
|
||||
for row in 0..NX_SUBMENU_ROWS {
|
||||
let menu_button_row = root_pane
|
||||
.find_pane_by_name_recursive(format!("TrModMenuButtonRow{row}").as_str())
|
||||
.unwrap();
|
||||
menu_button_row.set_visible(true);
|
||||
for col in 0..NX_SUBMENU_COLUMNS {
|
||||
if let Some(toggle) = submenu.toggles.get(row, col) {
|
||||
let menu_button = menu_button_row
|
||||
.find_pane_by_name_recursive(format!("Button{col}").as_str())
|
||||
.unwrap();
|
||||
menu_button.set_visible(true);
|
||||
let title_text = menu_button
|
||||
.find_pane_by_name_recursive("TitleTxt")
|
||||
.unwrap()
|
||||
.as_textbox();
|
||||
let title_bg = menu_button
|
||||
.find_pane_by_name_recursive("TitleBg")
|
||||
.unwrap()
|
||||
.as_picture();
|
||||
let title_bg_material = &mut *title_bg.material;
|
||||
let is_selected = row == submenu.toggles.state.selected_row().unwrap()
|
||||
&& col == submenu.toggles.state.selected_col().unwrap();
|
||||
|
||||
if is_selected {
|
||||
title_text.text_shadow_enable(true);
|
||||
title_text.text_outline_enable(true);
|
||||
|
||||
title_text.set_color(255, 255, 255, 255);
|
||||
|
||||
title_bg_material.set_white_res_color(BG_LEFT_ON_WHITE_COLOR);
|
||||
title_bg_material.set_black_res_color(BG_LEFT_ON_BLACK_COLOR);
|
||||
} else {
|
||||
title_text.text_shadow_enable(false);
|
||||
title_text.text_outline_enable(false);
|
||||
|
||||
title_text.set_color(178, 199, 211, 255);
|
||||
|
||||
title_bg_material.set_white_res_color(BG_LEFT_OFF_WHITE_COLOR);
|
||||
title_bg_material.set_black_res_color(BG_LEFT_OFF_BLACK_COLOR);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Hide all submenu icons, since we're not on the submenu page
|
||||
for t in tabs_clone.iter() {
|
||||
for s in t.submenus.iter() {
|
||||
let pane = menu_button.find_pane_by_name_recursive(s.id);
|
||||
if let Some(p) = pane {
|
||||
p.set_visible(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
title_text.set_text_string(toggle.title);
|
||||
|
||||
if use_check_icon {
|
||||
menu_button
|
||||
.find_pane_by_name_recursive("check")
|
||||
.unwrap()
|
||||
.set_visible(true);
|
||||
|
||||
menu_button
|
||||
.find_pane_by_name_recursive("Icon")
|
||||
.unwrap()
|
||||
.set_visible(toggle.value > 0);
|
||||
} else {
|
||||
menu_button
|
||||
.find_pane_by_name_recursive("check")
|
||||
.unwrap()
|
||||
.set_visible(false);
|
||||
menu_button
|
||||
.find_pane_by_name_recursive("Icon")
|
||||
.unwrap()
|
||||
.set_visible(toggle.value > 0);
|
||||
|
||||
// Note there's no pane for 0
|
||||
for value in 1..=toggle.max {
|
||||
let err_msg = format!("Could not find pane with name {}", value);
|
||||
menu_button
|
||||
.find_pane_by_name_recursive(format!("{}", value).as_str())
|
||||
.expect(&err_msg)
|
||||
.set_visible(value == toggle.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn render_slider_page(app: &App, root_pane: &Pane) {
|
||||
let (title, _help_text, gauge_vals) = app.sub_menu_strs_for_slider();
|
||||
let selected_min = gauge_vals.selected_min;
|
||||
let selected_max = gauge_vals.selected_max;
|
||||
unsafe fn render_slider_page(app: &mut App, root_pane: &Pane) {
|
||||
let submenu = app.selected_submenu();
|
||||
let slider = submenu.slider.unwrap();
|
||||
let selected_min = slider.lower;
|
||||
let selected_max = slider.upper;
|
||||
|
||||
let slider_pane = root_pane
|
||||
.find_pane_by_name_recursive("TrModSlider")
|
||||
|
@ -257,7 +273,7 @@ unsafe fn render_slider_page(app: &App, root_pane: &Pane) {
|
|||
.find_pane_by_name_recursive("Header")
|
||||
.unwrap()
|
||||
.as_textbox();
|
||||
header.set_text_string(title.as_str());
|
||||
header.set_text_string(submenu.title);
|
||||
let min_button = slider_pane
|
||||
.find_pane_by_name_recursive("MinButton")
|
||||
.unwrap()
|
||||
|
@ -292,8 +308,8 @@ unsafe fn render_slider_page(app: &App, root_pane: &Pane) {
|
|||
.as_textbox();
|
||||
|
||||
min_title_text.set_text_string("Min");
|
||||
match gauge_vals.state {
|
||||
GaugeState::MinHover | GaugeState::MinSelected => {
|
||||
match slider.state {
|
||||
SliderState::LowerHover | SliderState::LowerSelected => {
|
||||
min_title_text.text_shadow_enable(true);
|
||||
min_title_text.text_outline_enable(true);
|
||||
min_title_text.set_color(255, 255, 255, 255);
|
||||
|
@ -306,8 +322,8 @@ unsafe fn render_slider_page(app: &App, root_pane: &Pane) {
|
|||
}
|
||||
|
||||
max_title_text.set_text_string("Max");
|
||||
match gauge_vals.state {
|
||||
GaugeState::MaxHover | GaugeState::MaxSelected => {
|
||||
match slider.state {
|
||||
SliderState::UpperHover | SliderState::UpperSelected => {
|
||||
max_title_text.text_shadow_enable(true);
|
||||
max_title_text.text_outline_enable(true);
|
||||
max_title_text.set_color(255, 255, 255, 255);
|
||||
|
@ -323,9 +339,9 @@ unsafe fn render_slider_page(app: &App, root_pane: &Pane) {
|
|||
max_value_text.set_text_string(&format!("{selected_max}"));
|
||||
|
||||
let min_title_bg_material = &mut *min_title_bg.as_picture().material;
|
||||
let min_colors = match gauge_vals.state {
|
||||
GaugeState::MinHover => (BG_LEFT_ON_WHITE_COLOR, BG_LEFT_ON_BLACK_COLOR),
|
||||
GaugeState::MinSelected => (BG_LEFT_SELECTED_WHITE_COLOR, BG_LEFT_SELECTED_BLACK_COLOR),
|
||||
let min_colors = match slider.state {
|
||||
SliderState::LowerHover => (BG_LEFT_ON_WHITE_COLOR, BG_LEFT_ON_BLACK_COLOR),
|
||||
SliderState::LowerSelected => (BG_LEFT_SELECTED_WHITE_COLOR, BG_LEFT_SELECTED_BLACK_COLOR),
|
||||
_ => (BG_LEFT_OFF_WHITE_COLOR, BG_LEFT_OFF_BLACK_COLOR),
|
||||
};
|
||||
|
||||
|
@ -333,9 +349,9 @@ unsafe fn render_slider_page(app: &App, root_pane: &Pane) {
|
|||
min_title_bg_material.set_black_res_color(min_colors.1);
|
||||
|
||||
let max_title_bg_material = &mut *max_title_bg.as_picture().material;
|
||||
let max_colors = match gauge_vals.state {
|
||||
GaugeState::MaxHover => (BG_LEFT_ON_WHITE_COLOR, BG_LEFT_ON_BLACK_COLOR),
|
||||
GaugeState::MaxSelected => (BG_LEFT_SELECTED_WHITE_COLOR, BG_LEFT_SELECTED_BLACK_COLOR),
|
||||
let max_colors = match slider.state {
|
||||
SliderState::UpperHover => (BG_LEFT_ON_WHITE_COLOR, BG_LEFT_ON_BLACK_COLOR),
|
||||
SliderState::UpperSelected => (BG_LEFT_SELECTED_WHITE_COLOR, BG_LEFT_SELECTED_BLACK_COLOR),
|
||||
_ => (BG_LEFT_OFF_WHITE_COLOR, BG_LEFT_OFF_BLACK_COLOR),
|
||||
};
|
||||
|
||||
|
@ -352,6 +368,91 @@ unsafe fn render_slider_page(app: &App, root_pane: &Pane) {
|
|||
});
|
||||
}
|
||||
|
||||
unsafe fn render_confirmation_page(app: &mut App, root_pane: &Pane) {
|
||||
let show_row = 3; // Row in the middle of the page
|
||||
let show_cols = [1, 2]; // Columns in the middle of the page
|
||||
let no_col = show_cols[0]; // Left
|
||||
let yes_col = show_cols[1]; // Right
|
||||
let help_text = match app.confirmation_return_page {
|
||||
AppPage::TOGGLE | AppPage::SLIDER => {
|
||||
"Are you sure you want to reset the current setting to the defaults?"
|
||||
}
|
||||
AppPage::SUBMENU => "Are you sure you want to reset ALL settings to the defaults?",
|
||||
_ => "", // Shouldn't ever get this case, but don't panic if we do
|
||||
};
|
||||
|
||||
// Set help text
|
||||
root_pane
|
||||
.find_pane_by_name_recursive("FooterTxt")
|
||||
.unwrap()
|
||||
.as_textbox()
|
||||
.set_text_string(help_text);
|
||||
|
||||
// Show only the buttons that we care about
|
||||
for row in 0..NX_SUBMENU_ROWS {
|
||||
let should_show_row = row == show_row;
|
||||
let menu_button_row = root_pane
|
||||
.find_pane_by_name_recursive(format!("TrModMenuButtonRow{row}").as_str())
|
||||
.unwrap();
|
||||
menu_button_row.set_visible(should_show_row);
|
||||
if should_show_row {
|
||||
for col in 0..NX_SUBMENU_COLUMNS {
|
||||
let should_show_col = show_cols.contains(&col);
|
||||
let menu_button = menu_button_row
|
||||
.find_pane_by_name_recursive(format!("Button{col}").as_str())
|
||||
.unwrap();
|
||||
menu_button.set_visible(should_show_col);
|
||||
if should_show_col {
|
||||
let title_text = menu_button
|
||||
.find_pane_by_name_recursive("TitleTxt")
|
||||
.unwrap()
|
||||
.as_textbox();
|
||||
let title_bg = menu_button
|
||||
.find_pane_by_name_recursive("TitleBg")
|
||||
.unwrap()
|
||||
.as_picture();
|
||||
let title_bg_material = &mut *title_bg.material;
|
||||
|
||||
if col == no_col {
|
||||
title_text.set_text_string("No");
|
||||
} else if col == yes_col {
|
||||
title_text.set_text_string("Yes");
|
||||
}
|
||||
let is_selected = (col == no_col
|
||||
&& app.confirmation_state == ConfirmationState::HoverNo)
|
||||
|| (col == yes_col
|
||||
&& app.confirmation_state == ConfirmationState::HoverYes);
|
||||
|
||||
if is_selected {
|
||||
title_text.text_shadow_enable(true);
|
||||
title_text.text_outline_enable(true);
|
||||
title_text.set_color(255, 255, 255, 255);
|
||||
title_bg_material.set_white_res_color(BG_LEFT_ON_WHITE_COLOR);
|
||||
title_bg_material.set_black_res_color(BG_LEFT_ON_BLACK_COLOR);
|
||||
} else {
|
||||
title_text.text_shadow_enable(false);
|
||||
title_text.text_outline_enable(false);
|
||||
title_text.set_color(178, 199, 211, 255);
|
||||
title_bg_material.set_white_res_color(BG_LEFT_OFF_WHITE_COLOR);
|
||||
title_bg_material.set_black_res_color(BG_LEFT_OFF_BLACK_COLOR);
|
||||
}
|
||||
|
||||
// Hide all submenu icons, since we're not on the submenu page
|
||||
// TODO: Do we want to show the check on "Yes" and a red "X" on "No?"
|
||||
for t in app.tabs.iter() {
|
||||
for s in t.submenus.iter() {
|
||||
let pane = menu_button.find_pane_by_name_recursive(s.id);
|
||||
if let Some(p) = pane {
|
||||
p.set_visible(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub unsafe fn draw(root_pane: &Pane) {
|
||||
// Determine if we're in the menu by seeing if the "help" footer has
|
||||
// begun moving upward. It starts at -80 and moves to 0 over 10 frames
|
||||
|
@ -388,25 +489,24 @@ pub unsafe fn draw(root_pane: &Pane) {
|
|||
}
|
||||
|
||||
// Make all invisible first
|
||||
(0..NUM_MENU_TEXT_OPTIONS).for_each(|idx| {
|
||||
let col_idx = idx % NUM_LISTS;
|
||||
let row_idx = idx / NUM_LISTS;
|
||||
for row_idx in 0..NX_SUBMENU_ROWS {
|
||||
for col_idx in 0..NX_SUBMENU_COLUMNS {
|
||||
let menu_button_row = root_pane
|
||||
.find_pane_by_name_recursive(format!("TrModMenuButtonRow{row_idx}").as_str())
|
||||
.unwrap();
|
||||
menu_button_row.set_visible(false);
|
||||
|
||||
let menu_button_row = root_pane
|
||||
.find_pane_by_name_recursive(format!("TrModMenuButtonRow{row_idx}").as_str())
|
||||
.unwrap();
|
||||
menu_button_row.set_visible(false);
|
||||
let menu_button = menu_button_row
|
||||
.find_pane_by_name_recursive(format!("Button{col_idx}").as_str())
|
||||
.unwrap();
|
||||
menu_button.set_visible(false);
|
||||
|
||||
let menu_button = menu_button_row
|
||||
.find_pane_by_name_recursive(format!("Button{col_idx}").as_str())
|
||||
.unwrap();
|
||||
menu_button.set_visible(false);
|
||||
|
||||
menu_button
|
||||
.find_pane_by_name_recursive("ValueTxt")
|
||||
.unwrap()
|
||||
.set_visible(false);
|
||||
});
|
||||
menu_button
|
||||
.find_pane_by_name_recursive("ValueTxt")
|
||||
.unwrap()
|
||||
.set_visible(false);
|
||||
}
|
||||
}
|
||||
|
||||
// Make normal training panes invisible if we're active
|
||||
// InfluencedAlpha means "Should my children panes' alpha be influenced by mine, as the parent?"
|
||||
|
@ -423,21 +523,20 @@ pub unsafe fn draw(root_pane: &Pane) {
|
|||
|
||||
// Update menu display
|
||||
// Grabbing lock as read-only, essentially
|
||||
let app = &*crate::common::menu::QUICK_MENU_APP.data_ptr();
|
||||
// We don't really need to change anything, but get_before_selected requires &mut self
|
||||
let app = &mut *crate::common::menu::QUICK_MENU_APP.data_ptr();
|
||||
|
||||
let app_tabs = &app.tabs.items;
|
||||
let tab_selected = app.tabs.state.selected().unwrap();
|
||||
let prev_tab = if tab_selected == 0 {
|
||||
app_tabs.len() - 1
|
||||
} else {
|
||||
tab_selected - 1
|
||||
};
|
||||
let next_tab = if tab_selected == app_tabs.len() - 1 {
|
||||
0
|
||||
} else {
|
||||
tab_selected + 1
|
||||
};
|
||||
let tab_titles = [prev_tab, tab_selected, next_tab].map(|idx| app_tabs[idx].clone());
|
||||
let tab_titles = [
|
||||
app.tabs
|
||||
.get_before_selected()
|
||||
.expect("No tab selected!")
|
||||
.title,
|
||||
app.tabs.get_selected().expect("No tab selected!").title,
|
||||
app.tabs
|
||||
.get_after_selected()
|
||||
.expect("No tab selected!")
|
||||
.title,
|
||||
];
|
||||
|
||||
let is_gcc = (*common::menu::P1_CONTROLLER_STYLE.data_ptr()) == ControllerStyle::GCController;
|
||||
let button_mapping = if is_gcc {
|
||||
|
@ -456,12 +555,11 @@ pub unsafe fn draw(root_pane: &Pane) {
|
|||
button_mapping.get("Z"),
|
||||
);
|
||||
|
||||
let (left_tab_key, right_tab_key, save_defaults_key, reset_current_key, reset_all_key) =
|
||||
if is_gcc {
|
||||
(l_key, r_key, x_key, z_key, y_key)
|
||||
} else {
|
||||
(zl_key, zr_key, x_key, r_key, y_key)
|
||||
};
|
||||
let (left_tab_key, right_tab_key, save_defaults_key, reset_key, clear_toggle_key) = if is_gcc {
|
||||
(l_key, r_key, x_key, z_key, y_key)
|
||||
} else {
|
||||
(zl_key, zr_key, x_key, r_key, y_key)
|
||||
};
|
||||
|
||||
[
|
||||
(left_tab_key, "LeftTab"),
|
||||
|
@ -494,34 +592,77 @@ pub unsafe fn draw(root_pane: &Pane) {
|
|||
help_pane.set_default_material_colors();
|
||||
help_pane.set_color(255, 255, 0, 255);
|
||||
}
|
||||
help_pane.set_text_string(tab_titles[idx].as_str());
|
||||
help_pane.set_text_string(tab_titles[idx]);
|
||||
});
|
||||
[
|
||||
(save_defaults_key, "SaveDefaults", "Save Defaults"),
|
||||
(reset_current_key, "ResetCurrentDefaults", "Reset Current"),
|
||||
(reset_all_key, "ResetAllDefaults", "Reset All"),
|
||||
]
|
||||
.iter()
|
||||
.for_each(|(key, name, title)| {
|
||||
let key_help_pane = root_pane.find_pane_by_name_recursive(name).unwrap();
|
||||
|
||||
// Save Defaults Keyhelp
|
||||
let name = "SaveDefaults";
|
||||
let key = save_defaults_key;
|
||||
let title = "Save Defaults";
|
||||
let key_help_pane = root_pane.find_pane_by_name_recursive(name).unwrap();
|
||||
let icon_pane = key_help_pane
|
||||
.find_pane_by_name_recursive("set_txt_icon")
|
||||
.unwrap()
|
||||
.as_textbox();
|
||||
set_icon_text(icon_pane, &vec![*key.unwrap()]);
|
||||
key_help_pane
|
||||
.find_pane_by_name_recursive("set_txt_help")
|
||||
.unwrap()
|
||||
.as_textbox()
|
||||
.set_text_string(title);
|
||||
|
||||
// Reset Keyhelp
|
||||
let name = "ResetDefaults";
|
||||
let key = reset_key;
|
||||
let title = match app.page {
|
||||
AppPage::SUBMENU => "Reset All",
|
||||
AppPage::SLIDER => "Reset Current",
|
||||
AppPage::TOGGLE => "Reset Current",
|
||||
AppPage::CONFIRMATION => "",
|
||||
AppPage::CLOSE => "",
|
||||
};
|
||||
if !title.is_empty() {
|
||||
let key_help_pane = root_pane.find_pane_by_name_recursive(name).unwrap();
|
||||
let icon_pane = key_help_pane
|
||||
.find_pane_by_name_recursive("set_txt_icon")
|
||||
.unwrap()
|
||||
.as_textbox();
|
||||
set_icon_text(icon_pane, &vec![*key.unwrap()]);
|
||||
|
||||
key_help_pane
|
||||
.find_pane_by_name_recursive("set_txt_help")
|
||||
.unwrap()
|
||||
.as_textbox()
|
||||
.set_text_string(title);
|
||||
});
|
||||
}
|
||||
|
||||
// Clear Toggle Keyhelp
|
||||
let name = "ClearToggle";
|
||||
let key_help_pane = root_pane.find_pane_by_name_recursive(name).unwrap();
|
||||
let icon_pane = key_help_pane
|
||||
.find_pane_by_name_recursive("set_txt_icon")
|
||||
.unwrap();
|
||||
if app.should_show_clear_keyhelp() {
|
||||
// This is only displayed when you're in a multiple selection toggle menu w/ toggle.max > 1
|
||||
let key = clear_toggle_key;
|
||||
let title = "Clear Toggle";
|
||||
set_icon_text(icon_pane.as_textbox(), &vec![*key.unwrap()]);
|
||||
key_help_pane
|
||||
.find_pane_by_name_recursive("set_txt_help")
|
||||
.unwrap()
|
||||
.as_textbox()
|
||||
.set_text_string(title);
|
||||
icon_pane.set_visible(true);
|
||||
key_help_pane.set_visible(true);
|
||||
} else {
|
||||
icon_pane.set_visible(false);
|
||||
key_help_pane.set_visible(false);
|
||||
}
|
||||
|
||||
match app.page {
|
||||
AppPage::SUBMENU => render_submenu_page(app, root_pane),
|
||||
AppPage::SLIDER => render_slider_page(app, root_pane),
|
||||
AppPage::TOGGLE => render_toggle_page(app, root_pane),
|
||||
AppPage::CONFIRMATION => todo!(),
|
||||
AppPage::CONFIRMATION => render_confirmation_page(app, root_pane),
|
||||
AppPage::CLOSE => {}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -71,7 +71,7 @@ pub unsafe fn handle_draw(layout: *mut Layout, draw_info: u64, cmd_buffer: u64)
|
|||
{
|
||||
// InfluencedAlpha means "Should my children panes' alpha be influenced by mine, as the parent?"
|
||||
root_pane.flags |= 1 << PaneFlag::InfluencedAlpha as u8;
|
||||
root_pane.set_visible(MENU.hud == OnOff::On && !QUICK_MENU_ACTIVE);
|
||||
root_pane.set_visible(MENU.hud == OnOff::ON && !QUICK_MENU_ACTIVE);
|
||||
}
|
||||
|
||||
damage::draw(root_pane, &layout_name);
|
||||
|
|
|
@ -4,9 +4,7 @@ version = "0.1.0"
|
|||
edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
bitflags = "1.2.1"
|
||||
strum = "0.21.0"
|
||||
strum_macros = "0.21.0"
|
||||
byteflags = { git = "https://github.com/asimon-1/byteflags.git", branch = "rand-0.7.4" }
|
||||
num = "0.4.0"
|
||||
num-derive = "0.3"
|
||||
num-traits = "0.2"
|
||||
|
@ -14,11 +12,12 @@ paste = "1.0"
|
|||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_repr = "0.1.8"
|
||||
serde_json = "1"
|
||||
bitflags_serde_shim = "0.2"
|
||||
skyline = { git = "https://github.com/ultimate-research/skyline-rs.git" }
|
||||
skyline_smash = { git = "https://github.com/ultimate-research/skyline-smash.git", optional = true }
|
||||
toml = "0.5.9"
|
||||
anyhow = "1.0.72"
|
||||
rand = { git = "https://github.com/skyline-rs/rand" }
|
||||
training_mod_tui = { path = "../training_mod_tui"}
|
||||
|
||||
[features]
|
||||
default = ["smash"]
|
||||
|
|
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
@ -1,18 +1,18 @@
|
|||
[package]
|
||||
name = "training_mod_tui"
|
||||
version = "0.1.0"
|
||||
edition = "2018"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
[lib]
|
||||
doctest = false
|
||||
|
||||
[dependencies]
|
||||
tui = { version = "0.16.0", default-features = false }
|
||||
unicode-width = "0.1.9"
|
||||
training_mod_consts = { path = "../training_mod_consts", default-features = false}
|
||||
serde_json = "1.0.79"
|
||||
bitflags = "1.2.1"
|
||||
itertools = "0.11.0"
|
||||
ratatui = { git = "https://github.com/tonogdlp/ratatui.git", branch = "single-cell", default-features = false}
|
||||
serde = { version = "1.0.188", features = ["derive"] }
|
||||
serde_json = "1.0.106"
|
||||
crossterm = { version = "0.22.1", optional = true }
|
||||
|
||||
[features]
|
||||
default = []
|
||||
has_terminal = ["crossterm", "tui/crossterm"]
|
||||
has_terminal = ["crossterm", "ratatui/crossterm"]
|
390
training_mod_tui/src/containers/app.rs
Normal file
390
training_mod_tui/src/containers/app.rs
Normal file
|
@ -0,0 +1,390 @@
|
|||
use serde::ser::{SerializeMap, Serializer};
|
||||
use serde::Serialize;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::{InputControl, StatefulList, SubMenu, SubMenuType, Tab};
|
||||
|
||||
#[derive(PartialEq, Serialize, Clone, Copy)]
|
||||
pub enum AppPage {
|
||||
SUBMENU,
|
||||
TOGGLE,
|
||||
SLIDER,
|
||||
CONFIRMATION,
|
||||
CLOSE,
|
||||
}
|
||||
|
||||
#[derive(PartialEq)]
|
||||
pub enum ConfirmationState {
|
||||
HoverNo,
|
||||
HoverYes,
|
||||
}
|
||||
|
||||
impl ConfirmationState {
|
||||
pub fn switch(&self) -> ConfirmationState {
|
||||
match self {
|
||||
ConfirmationState::HoverNo => ConfirmationState::HoverYes,
|
||||
ConfirmationState::HoverYes => ConfirmationState::HoverNo,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Menu structure is:
|
||||
// App <StatefulTable<Tab>>
|
||||
// │
|
||||
// └─ Tab <StatefulTable<Submenu>>
|
||||
// │
|
||||
// └─ Submenu <Struct>
|
||||
// │
|
||||
// ├─ StatefulTable<Toggle>
|
||||
// │
|
||||
// │ OR
|
||||
// │
|
||||
// └─ Option<Slider>
|
||||
|
||||
pub struct App<'a> {
|
||||
pub tabs: StatefulList<Tab<'a>>,
|
||||
pub page: AppPage,
|
||||
pub serialized_settings: String,
|
||||
pub serialized_default_settings: String,
|
||||
pub confirmation_state: ConfirmationState,
|
||||
pub confirmation_return_page: AppPage,
|
||||
}
|
||||
|
||||
impl<'a> App<'a> {
|
||||
pub fn new() -> App<'a> {
|
||||
App {
|
||||
tabs: StatefulList::new(),
|
||||
page: AppPage::SUBMENU,
|
||||
serialized_settings: String::new(),
|
||||
serialized_default_settings: String::new(),
|
||||
confirmation_state: ConfirmationState::HoverNo,
|
||||
confirmation_return_page: AppPage::SUBMENU,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn current_settings_to_json(&self) -> String {
|
||||
serde_json::to_string(&self).expect("Could not serialize the menu to JSON!")
|
||||
}
|
||||
|
||||
pub fn get_serialized_settings_with_defaults(&self) -> String {
|
||||
format!(
|
||||
"{{\"menu\":{}, \"defaults_menu\":{}}}",
|
||||
self.serialized_settings, self.serialized_default_settings
|
||||
)
|
||||
}
|
||||
|
||||
pub fn save_settings(&mut self) {
|
||||
self.serialized_settings = self.current_settings_to_json();
|
||||
}
|
||||
|
||||
pub fn save_default_settings(&mut self) {
|
||||
self.serialized_default_settings = self.current_settings_to_json();
|
||||
}
|
||||
|
||||
pub fn load_defaults(&mut self) {
|
||||
// TODO!() is there a way to do this without cloning?
|
||||
let json = self.serialized_default_settings.clone();
|
||||
self.update_all_from_json(&json);
|
||||
}
|
||||
|
||||
pub fn load_defaults_for_current_submenu(&mut self) {
|
||||
let submenu_id = self.selected_submenu().id;
|
||||
let json = self.serialized_default_settings.clone();
|
||||
self.update_one_from_json(&json, submenu_id);
|
||||
}
|
||||
|
||||
pub fn update_all_from_json(&mut self, json: &str) {
|
||||
let all_settings: HashMap<String, Vec<u8>> =
|
||||
serde_json::from_str(json).expect("Could not parse the json!");
|
||||
for tab in self.tabs.iter_mut() {
|
||||
for submenu_opt in tab.submenus.iter_mut() {
|
||||
if let Some(submenu) = submenu_opt {
|
||||
if let Some(val) = all_settings.get(submenu.id) {
|
||||
submenu.update_from_vec(val.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
self.save_settings();
|
||||
}
|
||||
|
||||
#[allow(unused_labels)]
|
||||
pub fn update_one_from_json(&mut self, json: &str, submenu_id: &str) {
|
||||
let all_settings: HashMap<String, Vec<u8>> =
|
||||
serde_json::from_str(json).expect("Could not parse the json!");
|
||||
if let Some(val) = all_settings.get(submenu_id) {
|
||||
// No need to iterate through all the submenus if the id doesn't exist in the hashmap
|
||||
'tabs_scope: for tab in self.tabs.iter_mut() {
|
||||
'submenus_scope: for submenu_opt in tab.submenus.iter_mut() {
|
||||
if let Some(submenu) = submenu_opt {
|
||||
if submenu.id == submenu_id {
|
||||
submenu.update_from_vec(val.clone());
|
||||
break 'tabs_scope;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
self.save_settings();
|
||||
}
|
||||
|
||||
pub fn confirm(&mut self) -> bool {
|
||||
self.confirmation_state == ConfirmationState::HoverYes
|
||||
}
|
||||
|
||||
pub fn return_from_confirmation(&mut self) {
|
||||
self.confirmation_state = ConfirmationState::HoverNo;
|
||||
self.page = self.confirmation_return_page;
|
||||
}
|
||||
|
||||
pub fn selected_tab(&mut self) -> &mut Tab<'a> {
|
||||
self.tabs.get_selected().expect("No tab selected!")
|
||||
}
|
||||
|
||||
pub fn selected_submenu(&mut self) -> &mut SubMenu<'a> {
|
||||
self.selected_tab()
|
||||
.submenus
|
||||
.get_selected()
|
||||
.expect("No submenu selected!")
|
||||
}
|
||||
|
||||
pub fn should_show_clear_keyhelp(&mut self) -> bool {
|
||||
// Only show the "Clear Toggle" keyhelp if all of the following are true
|
||||
// 1. app.page is TOGGLE,
|
||||
// 2. selected_submenu.submenu_type is ToggleMultiple
|
||||
// 3. the toggle can be set to values greater than 1 (i.e. its not a boolean toggle)
|
||||
if self.page != AppPage::TOGGLE {
|
||||
return false;
|
||||
}
|
||||
let submenu = self.selected_submenu();
|
||||
match submenu.submenu_type {
|
||||
SubMenuType::ToggleMultiple => submenu.selected_toggle().max > 1,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Serialize for App<'a> {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
// Serializes as a mapping between submenu titles and values
|
||||
// Need to iterate through tabs to avoid making a list of mappings
|
||||
let len: usize = self.tabs.iter().map(|tab| tab.len()).sum();
|
||||
let mut map = serializer.serialize_map(Some(len))?;
|
||||
for tab in self.tabs.iter() {
|
||||
for submenu in tab.submenus.iter() {
|
||||
map.serialize_entry(submenu.id, submenu)?;
|
||||
}
|
||||
}
|
||||
map.end()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> InputControl for App<'a> {
|
||||
fn on_a(&mut self) {
|
||||
match self.page {
|
||||
AppPage::SUBMENU => {
|
||||
self.page = match self.selected_submenu().submenu_type {
|
||||
SubMenuType::ToggleSingle => AppPage::TOGGLE,
|
||||
SubMenuType::ToggleMultiple => AppPage::TOGGLE,
|
||||
SubMenuType::Slider => AppPage::SLIDER,
|
||||
};
|
||||
self.selected_tab().on_a()
|
||||
}
|
||||
AppPage::TOGGLE => self.selected_submenu().on_a(),
|
||||
AppPage::SLIDER => self.selected_submenu().on_a(),
|
||||
AppPage::CONFIRMATION => {
|
||||
// For resetting defaults
|
||||
// TODO: Is this the right place for this logic?
|
||||
if self.confirm() {
|
||||
match self.confirmation_return_page {
|
||||
AppPage::SUBMENU => {
|
||||
// Reset ALL settings to default
|
||||
self.load_defaults();
|
||||
}
|
||||
AppPage::TOGGLE | AppPage::SLIDER => {
|
||||
// Reset current submenu to default
|
||||
self.load_defaults_for_current_submenu();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
self.return_from_confirmation();
|
||||
}
|
||||
AppPage::CLOSE => {}
|
||||
}
|
||||
self.save_settings(); // A button can make changes, update the serialized settings
|
||||
}
|
||||
fn on_b(&mut self) {
|
||||
match self.page {
|
||||
AppPage::SUBMENU => {
|
||||
// Exit the app
|
||||
self.page = AppPage::CLOSE;
|
||||
}
|
||||
AppPage::TOGGLE => {
|
||||
// Return to the list of submenus
|
||||
self.page = AppPage::SUBMENU;
|
||||
}
|
||||
AppPage::SLIDER => {
|
||||
// Return to the list of submenus if we don't have a slider handle selected
|
||||
let slider = self
|
||||
.selected_submenu()
|
||||
.slider
|
||||
.as_mut()
|
||||
.expect("No slider selected!");
|
||||
if !slider.is_handle_selected() {
|
||||
self.page = AppPage::SUBMENU;
|
||||
} else {
|
||||
self.selected_submenu().on_b();
|
||||
}
|
||||
}
|
||||
AppPage::CONFIRMATION => {
|
||||
// Return to the list of submenus
|
||||
self.return_from_confirmation();
|
||||
}
|
||||
AppPage::CLOSE => {}
|
||||
}
|
||||
self.save_settings(); // B button can make changes, update the serialized settings
|
||||
}
|
||||
fn on_x(&mut self) {
|
||||
self.save_default_settings();
|
||||
}
|
||||
fn on_y(&mut self) {
|
||||
// Clear current toggle, for toggles w/ weighted selections
|
||||
match self.page {
|
||||
AppPage::TOGGLE => self.selected_submenu().on_y(),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
fn on_up(&mut self) {
|
||||
match self.page {
|
||||
AppPage::SUBMENU => self.tabs.get_selected().expect("No tab selected!").on_up(),
|
||||
AppPage::TOGGLE => self
|
||||
.tabs
|
||||
.get_selected()
|
||||
.expect("No tab selected!")
|
||||
.submenus
|
||||
.get_selected()
|
||||
.expect("No submenu selected!")
|
||||
.on_up(),
|
||||
AppPage::SLIDER => self
|
||||
.tabs
|
||||
.get_selected()
|
||||
.expect("No tab selected!")
|
||||
.submenus
|
||||
.get_selected()
|
||||
.expect("No submenu selected!")
|
||||
.on_up(),
|
||||
AppPage::CONFIRMATION => {}
|
||||
AppPage::CLOSE => {}
|
||||
}
|
||||
}
|
||||
fn on_down(&mut self) {
|
||||
match self.page {
|
||||
AppPage::SUBMENU => self
|
||||
.tabs
|
||||
.get_selected()
|
||||
.expect("No tab selected!")
|
||||
.on_down(),
|
||||
AppPage::TOGGLE => self
|
||||
.tabs
|
||||
.get_selected()
|
||||
.expect("No tab selected!")
|
||||
.submenus
|
||||
.get_selected()
|
||||
.expect("No submenu selected!")
|
||||
.on_down(),
|
||||
AppPage::SLIDER => self
|
||||
.tabs
|
||||
.get_selected()
|
||||
.expect("No tab selected!")
|
||||
.submenus
|
||||
.get_selected()
|
||||
.expect("No submenu selected!")
|
||||
.on_down(),
|
||||
AppPage::CONFIRMATION => {}
|
||||
AppPage::CLOSE => {}
|
||||
}
|
||||
}
|
||||
fn on_left(&mut self) {
|
||||
match self.page {
|
||||
AppPage::SUBMENU => self
|
||||
.tabs
|
||||
.get_selected()
|
||||
.expect("No tab selected!")
|
||||
.on_left(),
|
||||
AppPage::TOGGLE => self
|
||||
.tabs
|
||||
.get_selected()
|
||||
.expect("No tab selected!")
|
||||
.submenus
|
||||
.get_selected()
|
||||
.expect("No submenu selected!")
|
||||
.on_left(),
|
||||
AppPage::SLIDER => self
|
||||
.tabs
|
||||
.get_selected()
|
||||
.expect("No tab selected!")
|
||||
.submenus
|
||||
.get_selected()
|
||||
.expect("No submenu selected!")
|
||||
.on_left(),
|
||||
AppPage::CONFIRMATION => self.confirmation_state = self.confirmation_state.switch(),
|
||||
AppPage::CLOSE => {}
|
||||
}
|
||||
}
|
||||
fn on_right(&mut self) {
|
||||
match self.page {
|
||||
AppPage::SUBMENU => self
|
||||
.tabs
|
||||
.get_selected()
|
||||
.expect("No tab selected!")
|
||||
.on_right(),
|
||||
AppPage::TOGGLE => self
|
||||
.tabs
|
||||
.get_selected()
|
||||
.expect("No tab selected!")
|
||||
.submenus
|
||||
.get_selected()
|
||||
.expect("No submenu selected!")
|
||||
.on_right(),
|
||||
AppPage::SLIDER => self
|
||||
.tabs
|
||||
.get_selected()
|
||||
.expect("No tab selected!")
|
||||
.submenus
|
||||
.get_selected()
|
||||
.expect("No submenu selected!")
|
||||
.on_right(),
|
||||
AppPage::CONFIRMATION => self.confirmation_state = self.confirmation_state.switch(),
|
||||
AppPage::CLOSE => {}
|
||||
}
|
||||
}
|
||||
fn on_start(&mut self) {
|
||||
// Close menu
|
||||
self.page = AppPage::CLOSE;
|
||||
}
|
||||
fn on_l(&mut self) {}
|
||||
fn on_r(&mut self) {
|
||||
// Reset settings to default
|
||||
// See App::on_a() for the logic
|
||||
self.confirmation_return_page = self.page;
|
||||
self.page = AppPage::CONFIRMATION;
|
||||
}
|
||||
fn on_zl(&mut self) {
|
||||
match self.page {
|
||||
AppPage::SUBMENU => {
|
||||
self.tabs.previous();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
fn on_zr(&mut self) {
|
||||
match self.page {
|
||||
AppPage::SUBMENU => self.tabs.next(),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
24
training_mod_tui/src/containers/mod.rs
Normal file
24
training_mod_tui/src/containers/mod.rs
Normal file
|
@ -0,0 +1,24 @@
|
|||
mod app;
|
||||
mod submenu;
|
||||
mod tab;
|
||||
mod toggle;
|
||||
pub use app::*;
|
||||
pub use submenu::*;
|
||||
pub use tab::*;
|
||||
pub use toggle::*;
|
||||
|
||||
pub trait InputControl {
|
||||
fn on_a(&mut self);
|
||||
fn on_b(&mut self);
|
||||
fn on_x(&mut self);
|
||||
fn on_y(&mut self);
|
||||
fn on_up(&mut self);
|
||||
fn on_down(&mut self);
|
||||
fn on_left(&mut self);
|
||||
fn on_right(&mut self);
|
||||
fn on_start(&mut self);
|
||||
fn on_l(&mut self);
|
||||
fn on_r(&mut self);
|
||||
fn on_zl(&mut self);
|
||||
fn on_zr(&mut self);
|
||||
}
|
168
training_mod_tui/src/containers/submenu.rs
Normal file
168
training_mod_tui/src/containers/submenu.rs
Normal file
|
@ -0,0 +1,168 @@
|
|||
use serde::ser::Serializer;
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::{InputControl, StatefulSlider, StatefulTable, Toggle};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct SubMenu<'a> {
|
||||
pub title: &'a str,
|
||||
pub id: &'a str,
|
||||
pub help_text: &'a str,
|
||||
pub submenu_type: SubMenuType,
|
||||
pub toggles: StatefulTable<Toggle<'a>>,
|
||||
pub slider: Option<StatefulSlider>,
|
||||
}
|
||||
|
||||
impl<'a> Serialize for SubMenu<'a> {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
match self.submenu_type {
|
||||
SubMenuType::ToggleMultiple | SubMenuType::ToggleSingle => {
|
||||
self.toggles.serialize(serializer)
|
||||
}
|
||||
SubMenuType::Slider => self.slider.serialize(serializer),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> InputControl for SubMenu<'a> {
|
||||
fn on_a(&mut self) {
|
||||
match self.submenu_type {
|
||||
SubMenuType::ToggleSingle => {
|
||||
// Set all values to 0 first before incrementing the selected toggle
|
||||
// This ensure that exactly one toggle has a nonzero value
|
||||
for ind in 0..self.toggles.len() {
|
||||
self.toggles.get_by_idx_mut(ind).unwrap().value = 0;
|
||||
}
|
||||
self.selected_toggle().increment();
|
||||
}
|
||||
SubMenuType::ToggleMultiple => self.selected_toggle().increment(),
|
||||
SubMenuType::Slider => {
|
||||
let slider = self.slider.as_mut().expect("No slider selected!");
|
||||
slider.select_deselect();
|
||||
}
|
||||
}
|
||||
}
|
||||
fn on_b(&mut self) {
|
||||
match self.submenu_type {
|
||||
SubMenuType::ToggleSingle => {}
|
||||
SubMenuType::ToggleMultiple => {}
|
||||
SubMenuType::Slider => {
|
||||
let slider = self.slider.as_mut().expect("No slider selected!");
|
||||
if slider.is_handle_selected() {
|
||||
slider.deselect()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
fn on_x(&mut self) {}
|
||||
fn on_y(&mut self) {
|
||||
match self.submenu_type {
|
||||
SubMenuType::ToggleMultiple => {
|
||||
let toggle = self.selected_toggle();
|
||||
if toggle.max > 1 {
|
||||
toggle.value = 0;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
fn on_up(&mut self) {
|
||||
match self.submenu_type {
|
||||
SubMenuType::ToggleSingle => self.toggles.prev_row_checked(),
|
||||
SubMenuType::ToggleMultiple => self.toggles.prev_row_checked(),
|
||||
SubMenuType::Slider => {
|
||||
let slider = self.slider.as_mut().expect("No slider selected!");
|
||||
if slider.is_handle_selected() {
|
||||
slider.increment_selected_fast();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
fn on_down(&mut self) {
|
||||
match self.submenu_type {
|
||||
SubMenuType::ToggleSingle => self.toggles.next_row_checked(),
|
||||
SubMenuType::ToggleMultiple => self.toggles.next_row_checked(),
|
||||
SubMenuType::Slider => {
|
||||
let slider = self.slider.as_mut().expect("No slider selected!");
|
||||
if slider.is_handle_selected() {
|
||||
slider.decrement_selected_fast();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
fn on_left(&mut self) {
|
||||
match self.submenu_type {
|
||||
SubMenuType::ToggleSingle => self.toggles.prev_col_checked(),
|
||||
SubMenuType::ToggleMultiple => self.toggles.prev_col_checked(),
|
||||
SubMenuType::Slider => {
|
||||
let slider = self.slider.as_mut().expect("No slider selected!");
|
||||
if slider.is_handle_selected() {
|
||||
slider.decrement_selected_slow();
|
||||
} else {
|
||||
slider.switch_hover();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
fn on_right(&mut self) {
|
||||
match self.submenu_type {
|
||||
SubMenuType::ToggleSingle => self.toggles.next_col_checked(),
|
||||
SubMenuType::ToggleMultiple => self.toggles.next_col_checked(),
|
||||
SubMenuType::Slider => {
|
||||
let slider = self.slider.as_mut().expect("No slider selected!");
|
||||
if slider.is_handle_selected() {
|
||||
slider.increment_selected_slow();
|
||||
} else {
|
||||
slider.switch_hover();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
fn on_start(&mut self) {}
|
||||
fn on_l(&mut self) {}
|
||||
fn on_r(&mut self) {}
|
||||
fn on_zl(&mut self) {}
|
||||
fn on_zr(&mut self) {}
|
||||
}
|
||||
|
||||
impl<'a> SubMenu<'a> {
|
||||
pub fn selected_toggle(&mut self) -> &mut Toggle<'a> {
|
||||
self.toggles.get_selected().expect("No toggle selected!")
|
||||
}
|
||||
|
||||
pub fn update_from_vec(&mut self, values: Vec<u8>) {
|
||||
match self.submenu_type {
|
||||
SubMenuType::ToggleSingle | SubMenuType::ToggleMultiple => {
|
||||
for (idx, value) in values.iter().enumerate() {
|
||||
if let Some(toggle) = self.toggles.get_by_idx_mut(idx) {
|
||||
toggle.value = *value;
|
||||
}
|
||||
}
|
||||
}
|
||||
SubMenuType::Slider => {
|
||||
assert_eq!(
|
||||
values.len(),
|
||||
2,
|
||||
"Exactly two values need to be passed to submenu.set() for slider!"
|
||||
);
|
||||
if let Some(s) = self.slider {
|
||||
self.slider = Some(StatefulSlider {
|
||||
lower: values[0].into(),
|
||||
upper: values[1].into(),
|
||||
..s
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Serialize)]
|
||||
pub enum SubMenuType {
|
||||
ToggleSingle,
|
||||
ToggleMultiple,
|
||||
Slider,
|
||||
}
|
54
training_mod_tui/src/containers/tab.rs
Normal file
54
training_mod_tui/src/containers/tab.rs
Normal file
|
@ -0,0 +1,54 @@
|
|||
use serde::ser::{SerializeMap, Serializer};
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::{InputControl, StatefulTable, SubMenu};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Tab<'a> {
|
||||
pub title: &'a str,
|
||||
pub id: &'a str,
|
||||
pub submenus: StatefulTable<SubMenu<'a>>,
|
||||
}
|
||||
|
||||
impl<'a> Tab<'a> {
|
||||
pub fn len(&self) -> usize {
|
||||
self.submenus.len()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Serialize for Tab<'a> {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
let mut map = serializer.serialize_map(Some(self.submenus.len()))?;
|
||||
for submenu in self.submenus.as_vec().iter() {
|
||||
map.serialize_entry(&submenu.title, &submenu)?;
|
||||
}
|
||||
map.end()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> InputControl for Tab<'a> {
|
||||
fn on_a(&mut self) {}
|
||||
fn on_b(&mut self) {}
|
||||
fn on_x(&mut self) {}
|
||||
fn on_y(&mut self) {}
|
||||
fn on_up(&mut self) {
|
||||
self.submenus.prev_row_checked()
|
||||
}
|
||||
fn on_down(&mut self) {
|
||||
self.submenus.next_row_checked()
|
||||
}
|
||||
fn on_left(&mut self) {
|
||||
self.submenus.prev_col_checked()
|
||||
}
|
||||
fn on_right(&mut self) {
|
||||
self.submenus.next_col_checked()
|
||||
}
|
||||
fn on_start(&mut self) {}
|
||||
fn on_l(&mut self) {}
|
||||
fn on_r(&mut self) {}
|
||||
fn on_zl(&mut self) {}
|
||||
fn on_zr(&mut self) {}
|
||||
}
|
36
training_mod_tui/src/containers/toggle.rs
Normal file
36
training_mod_tui/src/containers/toggle.rs
Normal file
|
@ -0,0 +1,36 @@
|
|||
use serde::ser::Serializer;
|
||||
use serde::Serialize;
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct Toggle<'a> {
|
||||
pub title: &'a str,
|
||||
pub value: u8,
|
||||
pub max: u8,
|
||||
}
|
||||
|
||||
impl<'a> Serialize for Toggle<'a> {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_u8(self.value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Toggle<'a> {
|
||||
pub fn increment(&mut self) {
|
||||
if self.value == self.max {
|
||||
self.value = 0;
|
||||
} else {
|
||||
self.value += 1;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn decrement(&mut self) {
|
||||
if self.value == 0 {
|
||||
self.value = self.max;
|
||||
} else {
|
||||
self.value -= 1;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,27 +0,0 @@
|
|||
pub enum GaugeState {
|
||||
MinHover,
|
||||
MaxHover,
|
||||
MinSelected,
|
||||
MaxSelected,
|
||||
None,
|
||||
}
|
||||
|
||||
pub struct DoubleEndedGauge {
|
||||
pub state: GaugeState,
|
||||
pub selected_min: u32,
|
||||
pub selected_max: u32,
|
||||
pub abs_min: u32,
|
||||
pub abs_max: u32,
|
||||
}
|
||||
|
||||
impl DoubleEndedGauge {
|
||||
pub fn new() -> DoubleEndedGauge {
|
||||
DoubleEndedGauge {
|
||||
state: GaugeState::None,
|
||||
selected_min: 0,
|
||||
selected_max: 150,
|
||||
abs_min: 0,
|
||||
abs_max: 150,
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,911 +1,7 @@
|
|||
use training_mod_consts::{
|
||||
ui_menu, MenuJsonStruct, Slider, SubMenu, SubMenuType, Toggle, TrainingModpackMenu, UiMenu,
|
||||
};
|
||||
use tui::{
|
||||
backend::Backend,
|
||||
layout::{Constraint, Corner, Direction, Layout, Rect},
|
||||
style::{Modifier, Style},
|
||||
text::{Span, Spans},
|
||||
widgets::{Block, LineGauge, List, ListItem, ListState, Paragraph, Tabs},
|
||||
Frame,
|
||||
};
|
||||
mod containers;
|
||||
pub use containers::*;
|
||||
mod structures;
|
||||
pub use structures::*;
|
||||
|
||||
use serde_json::{json, Map};
|
||||
use std::collections::HashMap;
|
||||
pub use tui::{backend::TestBackend, style::Color, Terminal};
|
||||
|
||||
pub mod gauge;
|
||||
mod list;
|
||||
|
||||
use crate::gauge::{DoubleEndedGauge, GaugeState};
|
||||
use crate::list::{MultiStatefulList, StatefulList};
|
||||
|
||||
static NX_TUI_WIDTH: u16 = 240;
|
||||
// Number of lists per page
|
||||
pub const NUM_LISTS: usize = 4;
|
||||
|
||||
#[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
|
||||
pub struct App {
|
||||
pub tabs: StatefulList<String>,
|
||||
pub menu_items: HashMap<String, MultiStatefulList<SubMenu>>,
|
||||
pub selected_sub_menu_toggles: MultiStatefulList<Toggle>,
|
||||
pub selected_sub_menu_slider: DoubleEndedGauge,
|
||||
pub page: AppPage,
|
||||
pub default_menu: (UiMenu, String),
|
||||
}
|
||||
|
||||
impl<'a> App {
|
||||
pub fn new(menu: UiMenu, default_menu: (UiMenu, String)) -> App {
|
||||
let mut menu_items_stateful = HashMap::new();
|
||||
menu.tabs.iter().for_each(|tab| {
|
||||
menu_items_stateful.insert(
|
||||
tab.tab_title.clone(),
|
||||
MultiStatefulList::with_items(tab.tab_submenus.clone(), NUM_LISTS),
|
||||
);
|
||||
});
|
||||
|
||||
let mut app = App {
|
||||
tabs: StatefulList::with_items(
|
||||
menu.tabs.iter().map(|tab| tab.tab_title.clone()).collect(),
|
||||
),
|
||||
menu_items: menu_items_stateful,
|
||||
selected_sub_menu_toggles: MultiStatefulList::with_items(vec![], 0),
|
||||
selected_sub_menu_slider: DoubleEndedGauge::new(),
|
||||
page: AppPage::SUBMENU,
|
||||
default_menu: default_menu,
|
||||
};
|
||||
app.set_sub_menu_items();
|
||||
app
|
||||
}
|
||||
|
||||
/// Takes the currently selected tab/submenu and clones the options into
|
||||
/// self.selected_sub_menu_toggles and self.selected_sub_menu_slider
|
||||
pub fn set_sub_menu_items(&mut self) {
|
||||
let (list_section, list_idx) = self
|
||||
.menu_items
|
||||
.get(self.tab_selected())
|
||||
.unwrap()
|
||||
.idx_to_list_idx(self.menu_items.get(self.tab_selected()).unwrap().state);
|
||||
let selected_sub_menu = &self.menu_items.get(self.tab_selected()).unwrap().lists
|
||||
[list_section]
|
||||
.items
|
||||
.get(list_idx)
|
||||
.unwrap();
|
||||
|
||||
let toggles = selected_sub_menu.toggles.clone();
|
||||
let slider = selected_sub_menu.slider.clone();
|
||||
match SubMenuType::from_string(&self.sub_menu_selected()._type) {
|
||||
SubMenuType::TOGGLE => {
|
||||
self.selected_sub_menu_toggles = MultiStatefulList::with_items(
|
||||
toggles,
|
||||
if selected_sub_menu.toggles.len() >= NUM_LISTS {
|
||||
NUM_LISTS
|
||||
} else {
|
||||
selected_sub_menu.toggles.len()
|
||||
},
|
||||
)
|
||||
}
|
||||
SubMenuType::SLIDER => {
|
||||
let slider = slider.unwrap();
|
||||
self.selected_sub_menu_slider = DoubleEndedGauge {
|
||||
state: GaugeState::None,
|
||||
selected_min: slider.selected_min,
|
||||
selected_max: slider.selected_max,
|
||||
abs_min: slider.abs_min,
|
||||
abs_max: slider.abs_max,
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// Returns the id of the currently selected tab
|
||||
pub fn tab_selected(&self) -> &str {
|
||||
self.tabs
|
||||
.items
|
||||
.get(self.tabs.state.selected().unwrap())
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
/// Returns the currently selected SubMenu struct
|
||||
///
|
||||
/// {
|
||||
/// submenu_title: String,
|
||||
/// submenu_id: String,
|
||||
/// help_text: String,
|
||||
/// is_single_option: bool,
|
||||
/// toggles: Vec<Toggle<'a>>,
|
||||
/// slider: Option<Slider>,
|
||||
/// _type: String,
|
||||
/// }
|
||||
fn sub_menu_selected(&self) -> &SubMenu {
|
||||
let (list_section, list_idx) = self
|
||||
.menu_items
|
||||
.get(self.tab_selected())
|
||||
.unwrap()
|
||||
.idx_to_list_idx(self.menu_items.get(self.tab_selected()).unwrap().state);
|
||||
self.menu_items.get(self.tab_selected()).unwrap().lists[list_section]
|
||||
.items
|
||||
.get(list_idx)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
/// A "next()" function which differs per submenu type
|
||||
/// Toggles: calls next()
|
||||
/// Slider: Swaps between MinHover and MaxHover
|
||||
pub fn sub_menu_next(&mut self) {
|
||||
match SubMenuType::from_string(&self.sub_menu_selected()._type) {
|
||||
SubMenuType::TOGGLE => self.selected_sub_menu_toggles.next(),
|
||||
SubMenuType::SLIDER => match self.selected_sub_menu_slider.state {
|
||||
GaugeState::MinHover => self.selected_sub_menu_slider.state = GaugeState::MaxHover,
|
||||
GaugeState::MaxHover => self.selected_sub_menu_slider.state = GaugeState::MinHover,
|
||||
_ => {}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// A "next_list()" function which differs per submenu type
|
||||
/// Toggles: Calls next_list()
|
||||
/// Slider:
|
||||
/// * Swaps between MinHover and MaxHover
|
||||
/// * Increments the selected_min/max if possible
|
||||
pub fn sub_menu_next_list(&mut self) {
|
||||
match SubMenuType::from_string(&self.sub_menu_selected()._type) {
|
||||
SubMenuType::TOGGLE => self.selected_sub_menu_toggles.next_list(),
|
||||
SubMenuType::SLIDER => match self.selected_sub_menu_slider.state {
|
||||
GaugeState::MinHover => self.selected_sub_menu_slider.state = GaugeState::MaxHover,
|
||||
GaugeState::MaxHover => self.selected_sub_menu_slider.state = GaugeState::MinHover,
|
||||
GaugeState::MinSelected => {
|
||||
if self.selected_sub_menu_slider.selected_min
|
||||
< self.selected_sub_menu_slider.selected_max
|
||||
{
|
||||
self.selected_sub_menu_slider.selected_min += 1;
|
||||
}
|
||||
}
|
||||
GaugeState::MaxSelected => {
|
||||
if self.selected_sub_menu_slider.selected_max
|
||||
< self.selected_sub_menu_slider.abs_max
|
||||
{
|
||||
self.selected_sub_menu_slider.selected_max += 1;
|
||||
}
|
||||
}
|
||||
GaugeState::None => {}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// A "previous()" function which differs per submenu type
|
||||
/// Toggles: calls previous()
|
||||
/// Slider: Swaps between MinHover and MaxHover
|
||||
pub fn sub_menu_previous(&mut self) {
|
||||
match SubMenuType::from_string(&self.sub_menu_selected()._type) {
|
||||
SubMenuType::TOGGLE => self.selected_sub_menu_toggles.previous(),
|
||||
SubMenuType::SLIDER => match self.selected_sub_menu_slider.state {
|
||||
GaugeState::MinHover => self.selected_sub_menu_slider.state = GaugeState::MaxHover,
|
||||
GaugeState::MaxHover => self.selected_sub_menu_slider.state = GaugeState::MinHover,
|
||||
_ => {}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// A "previous_list()" function which differs per submenu type
|
||||
/// Toggles: Calls previous_list()
|
||||
/// Slider:
|
||||
/// * Swaps between MinHover and MaxHover
|
||||
/// * Decrements the selected_min/max if possible
|
||||
pub fn sub_menu_previous_list(&mut self) {
|
||||
match SubMenuType::from_string(&self.sub_menu_selected()._type) {
|
||||
SubMenuType::TOGGLE => self.selected_sub_menu_toggles.previous_list(),
|
||||
SubMenuType::SLIDER => match self.selected_sub_menu_slider.state {
|
||||
GaugeState::MinHover => self.selected_sub_menu_slider.state = GaugeState::MaxHover,
|
||||
GaugeState::MaxHover => self.selected_sub_menu_slider.state = GaugeState::MinHover,
|
||||
GaugeState::MinSelected => {
|
||||
if self.selected_sub_menu_slider.selected_min
|
||||
> self.selected_sub_menu_slider.abs_min
|
||||
{
|
||||
self.selected_sub_menu_slider.selected_min -= 1;
|
||||
}
|
||||
}
|
||||
GaugeState::MaxSelected => {
|
||||
if self.selected_sub_menu_slider.selected_max
|
||||
> self.selected_sub_menu_slider.selected_min
|
||||
{
|
||||
self.selected_sub_menu_slider.selected_max -= 1;
|
||||
}
|
||||
}
|
||||
GaugeState::None => {}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns information about the currently selected submenu
|
||||
///
|
||||
/// 0: Submenu Title
|
||||
/// 1: Submenu Help Text
|
||||
/// 2: Vec(toggle checked, title) for toggles, Vec(nothing) for slider
|
||||
/// 3: ListState for toggles, ListState::new() for slider
|
||||
/// TODO: Refactor return type into a nice struct
|
||||
pub fn sub_menu_strs_and_states(
|
||||
&self,
|
||||
) -> (String, String, Vec<(Vec<(bool, String)>, ListState)>) {
|
||||
(
|
||||
self.sub_menu_selected().submenu_title.clone(),
|
||||
self.sub_menu_selected().help_text.clone(),
|
||||
match SubMenuType::from_string(&self.sub_menu_selected()._type) {
|
||||
SubMenuType::TOGGLE => self
|
||||
.selected_sub_menu_toggles
|
||||
.lists
|
||||
.iter()
|
||||
.map(|toggle_list| {
|
||||
(
|
||||
toggle_list
|
||||
.items
|
||||
.iter()
|
||||
.map(|toggle| (toggle.checked, toggle.toggle_title.clone()))
|
||||
.collect(),
|
||||
toggle_list.state.clone(),
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
SubMenuType::SLIDER => {
|
||||
vec![(vec![], ListState::default())]
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/// Returns information about the currently selected slider
|
||||
/// 0: Title
|
||||
/// 1: Help text
|
||||
/// 2: Reference to self.selected_sub_menu_slider
|
||||
/// TODO: Refactor return type into a nice struct
|
||||
pub fn sub_menu_strs_for_slider(&self) -> (String, String, &DoubleEndedGauge) {
|
||||
let slider = match SubMenuType::from_string(&self.sub_menu_selected()._type) {
|
||||
SubMenuType::SLIDER => &self.selected_sub_menu_slider,
|
||||
_ => {
|
||||
panic!("Slider not selected!");
|
||||
}
|
||||
};
|
||||
(
|
||||
self.sub_menu_selected().submenu_title.clone(),
|
||||
self.sub_menu_selected().help_text.clone(),
|
||||
slider,
|
||||
)
|
||||
}
|
||||
|
||||
/// Different behavior depending on the current menu location
|
||||
/// 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) {
|
||||
let tab_selected = self
|
||||
.tabs
|
||||
.items
|
||||
.get(self.tabs.state.selected().unwrap())
|
||||
.unwrap();
|
||||
let (list_section, list_idx) = self
|
||||
.menu_items
|
||||
.get(tab_selected)
|
||||
.unwrap()
|
||||
.idx_to_list_idx(self.menu_items.get(tab_selected).unwrap().state);
|
||||
let selected_sub_menu = self.menu_items.get_mut(tab_selected).unwrap().lists[list_section]
|
||||
.items
|
||||
.get_mut(list_idx)
|
||||
.unwrap();
|
||||
if self.page == AppPage::SUBMENU {
|
||||
match SubMenuType::from_string(&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_string(&selected_sub_menu._type) {
|
||||
SubMenuType::TOGGLE => {
|
||||
let is_single_option = selected_sub_menu.is_single_option;
|
||||
let state = self.selected_sub_menu_toggles.state;
|
||||
// Change the toggles in self.selected_sub_menu_toggles (for display)
|
||||
self.selected_sub_menu_toggles
|
||||
.lists
|
||||
.iter_mut()
|
||||
.map(|list| (list.state.selected(), &mut list.items))
|
||||
.for_each(|(state, toggle_list)| {
|
||||
toggle_list.iter_mut().enumerate().for_each(|(i, o)| {
|
||||
if state.is_some() && i == state.unwrap() {
|
||||
if !o.checked {
|
||||
o.checked = true;
|
||||
} else {
|
||||
if is_single_option {
|
||||
return;
|
||||
}
|
||||
o.checked = false;
|
||||
}
|
||||
} else if is_single_option {
|
||||
o.checked = false;
|
||||
}
|
||||
})
|
||||
});
|
||||
// Actually change the toggle values in the SubMenu struct
|
||||
selected_sub_menu
|
||||
.toggles
|
||||
.iter_mut()
|
||||
.enumerate()
|
||||
.for_each(|(i, o)| {
|
||||
if i == state {
|
||||
if !o.checked {
|
||||
o.checked = true;
|
||||
} else {
|
||||
if is_single_option {
|
||||
return;
|
||||
}
|
||||
o.checked = false;
|
||||
}
|
||||
} else if is_single_option {
|
||||
o.checked = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
SubMenuType::SLIDER => match self.selected_sub_menu_slider.state {
|
||||
GaugeState::MinHover => {
|
||||
self.selected_sub_menu_slider.state = GaugeState::MinSelected;
|
||||
}
|
||||
GaugeState::MaxHover => {
|
||||
self.selected_sub_menu_slider.state = GaugeState::MaxSelected;
|
||||
}
|
||||
GaugeState::MinSelected => {
|
||||
self.selected_sub_menu_slider.state = GaugeState::MinHover;
|
||||
selected_sub_menu.slider = Some(Slider {
|
||||
selected_min: self.selected_sub_menu_slider.selected_min,
|
||||
selected_max: self.selected_sub_menu_slider.selected_max,
|
||||
abs_min: self.selected_sub_menu_slider.abs_min,
|
||||
abs_max: self.selected_sub_menu_slider.abs_max,
|
||||
});
|
||||
}
|
||||
GaugeState::MaxSelected => {
|
||||
self.selected_sub_menu_slider.state = GaugeState::MaxHover;
|
||||
selected_sub_menu.slider = Some(Slider {
|
||||
selected_min: self.selected_sub_menu_slider.selected_min,
|
||||
selected_max: self.selected_sub_menu_slider.selected_max,
|
||||
abs_min: self.selected_sub_menu_slider.abs_min,
|
||||
abs_max: self.selected_sub_menu_slider.abs_max,
|
||||
});
|
||||
}
|
||||
GaugeState::None => {
|
||||
self.selected_sub_menu_slider.state = GaugeState::MinHover;
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Different behavior depending on the current menu location
|
||||
/// 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
|
||||
.items
|
||||
.get(self.tabs.state.selected().unwrap())
|
||||
.unwrap();
|
||||
let (list_section, list_idx) = self
|
||||
.menu_items
|
||||
.get(tab_selected)
|
||||
.unwrap()
|
||||
.idx_to_list_idx(self.menu_items.get(tab_selected).unwrap().state);
|
||||
let selected_sub_menu = self.menu_items.get_mut(tab_selected).unwrap().lists[list_section]
|
||||
.items
|
||||
.get_mut(list_idx)
|
||||
.unwrap();
|
||||
match SubMenuType::from_string(&selected_sub_menu._type) {
|
||||
SubMenuType::SLIDER => match self.selected_sub_menu_slider.state {
|
||||
GaugeState::MinSelected => {
|
||||
self.selected_sub_menu_slider.state = GaugeState::MinHover;
|
||||
selected_sub_menu.slider = Some(Slider {
|
||||
selected_min: self.selected_sub_menu_slider.selected_min,
|
||||
selected_max: self.selected_sub_menu_slider.selected_max,
|
||||
abs_min: self.selected_sub_menu_slider.abs_min,
|
||||
abs_max: self.selected_sub_menu_slider.abs_max,
|
||||
});
|
||||
// Don't go back to the outer list
|
||||
return;
|
||||
}
|
||||
GaugeState::MaxSelected => {
|
||||
self.selected_sub_menu_slider.state = GaugeState::MaxHover;
|
||||
selected_sub_menu.slider = Some(Slider {
|
||||
selected_min: self.selected_sub_menu_slider.selected_min,
|
||||
selected_max: self.selected_sub_menu_slider.selected_max,
|
||||
abs_min: self.selected_sub_menu_slider.abs_min,
|
||||
abs_max: self.selected_sub_menu_slider.abs_max,
|
||||
});
|
||||
// Don't go back to the outer list
|
||||
return;
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
self.page = AppPage::SUBMENU;
|
||||
self.set_sub_menu_items();
|
||||
}
|
||||
|
||||
/// Save defaults command
|
||||
pub fn save_defaults(&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 reset_current_submenu(&mut self) {
|
||||
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.clone();
|
||||
let default_json_value =
|
||||
serde_json::from_str::<serde_json::Value>(&self.default_menu.1).unwrap();
|
||||
*json_value.get_mut(id.as_str()).unwrap() =
|
||||
default_json_value.get(id.as_str()).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 reset_all_submenus(&mut self) {
|
||||
*self = App::new(self.default_menu.0.clone(), self.default_menu.clone());
|
||||
}
|
||||
|
||||
pub fn previous_tab(&mut self) {
|
||||
if self.page == AppPage::SUBMENU {
|
||||
self.tabs.previous();
|
||||
self.set_sub_menu_items();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn next_tab(&mut self) {
|
||||
if self.page == AppPage::SUBMENU {
|
||||
self.tabs.next();
|
||||
self.set_sub_menu_items();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn on_up(&mut self) {
|
||||
if self.page == AppPage::SUBMENU {
|
||||
self.menu_items
|
||||
.get_mut(
|
||||
self.tabs
|
||||
.items
|
||||
.get(self.tabs.state.selected().unwrap())
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap()
|
||||
.previous();
|
||||
self.set_sub_menu_items();
|
||||
} else if self.page == AppPage::TOGGLE || self.page == AppPage::SLIDER {
|
||||
self.sub_menu_previous();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn on_down(&mut self) {
|
||||
if self.page == AppPage::SUBMENU {
|
||||
self.menu_items
|
||||
.get_mut(
|
||||
self.tabs
|
||||
.items
|
||||
.get(self.tabs.state.selected().unwrap())
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap()
|
||||
.next();
|
||||
self.set_sub_menu_items();
|
||||
} else if self.page == AppPage::TOGGLE || self.page == AppPage::SLIDER {
|
||||
self.sub_menu_next();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn on_left(&mut self) {
|
||||
if self.page == AppPage::SUBMENU {
|
||||
self.menu_items
|
||||
.get_mut(
|
||||
self.tabs
|
||||
.items
|
||||
.get(self.tabs.state.selected().unwrap())
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap()
|
||||
.previous_list();
|
||||
self.set_sub_menu_items();
|
||||
} else if self.page == AppPage::TOGGLE || self.page == AppPage::SLIDER {
|
||||
self.sub_menu_previous_list();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn on_right(&mut self) {
|
||||
if self.page == AppPage::SUBMENU {
|
||||
self.menu_items
|
||||
.get_mut(
|
||||
self.tabs
|
||||
.items
|
||||
.get(self.tabs.state.selected().unwrap())
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap()
|
||||
.next_list();
|
||||
self.set_sub_menu_items();
|
||||
} 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 submenu_ids(&self) -> Vec<String> {
|
||||
return self
|
||||
.menu_items
|
||||
.values()
|
||||
.flat_map(|multi_stateful_list| {
|
||||
multi_stateful_list
|
||||
.lists
|
||||
.iter()
|
||||
.flat_map(|sub_stateful_list| {
|
||||
sub_stateful_list
|
||||
.items
|
||||
.iter()
|
||||
.map(|submenu| submenu.submenu_id.clone())
|
||||
})
|
||||
})
|
||||
.collect::<Vec<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.clone()
|
||||
} else {
|
||||
format!(" {}", i.submenu_title.clone())
|
||||
})];
|
||||
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
|
||||
.clone(),
|
||||
);
|
||||
}
|
||||
|
||||
f.render_stateful_widget(list, list_chunks[list_section], &mut state);
|
||||
}
|
||||
|
||||
let help_paragraph = Paragraph::new(
|
||||
item_help.unwrap_or("".to_string()).replace('\"', "")
|
||||
+ "\nZL/ZR: Next tab | X: Save Defaults | R: Reset All Menus",
|
||||
)
|
||||
.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 {
|
||||
format!("X {}", s.1)
|
||||
} else {
|
||||
format!(" {}", s.1)
|
||||
})])
|
||||
})
|
||||
.collect();
|
||||
|
||||
let values_list = List::new(values_items)
|
||||
.block(Block::default().title(if list_section == 0 {
|
||||
title.clone()
|
||||
} else {
|
||||
"".to_string()
|
||||
}))
|
||||
.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('\"', "") + "\nL: Reset Current Menu")
|
||||
.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('\"', "") + "\nL: Reset Current Menu")
|
||||
.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();
|
||||
|
||||
let titles: Vec<Spans> = app_tabs
|
||||
.items
|
||||
.iter()
|
||||
.cloned()
|
||||
.enumerate()
|
||||
.map(|(idx, tab)| {
|
||||
if idx == tab_selected {
|
||||
span_selected = Spans::from(format!("> {}", tab));
|
||||
Spans::from(format!("> {}", tab))
|
||||
} else {
|
||||
Spans::from(format!(" {}", tab))
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
// There is only enough room to display 3 tabs of text
|
||||
// So lets replace tabs not near the selected with "..."
|
||||
let all_windows: Vec<&[Spans]> = titles
|
||||
.windows(3)
|
||||
.filter(|w| w.contains(&titles[tab_selected]))
|
||||
.collect();
|
||||
let first_window = all_windows[0];
|
||||
let mut titles: Vec<Spans> = titles
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(
|
||||
// Converts all tabs not in the window to "..."
|
||||
|t| {
|
||||
if first_window.contains(&t) {
|
||||
t
|
||||
} else {
|
||||
Spans::from("...".to_owned())
|
||||
}
|
||||
},
|
||||
)
|
||||
.collect();
|
||||
// Don't keep consecutive "..." tabs
|
||||
titles.dedup();
|
||||
// Now that the size of the titles vector has changed, need to re-locate the selected tab
|
||||
let tab_selected_deduped: usize = titles
|
||||
.iter()
|
||||
.cloned()
|
||||
.position(|span| span == span_selected)
|
||||
.unwrap_or(0);
|
||||
|
||||
let tabs = Tabs::new(titles)
|
||||
.block(Block::default().title(Spans::from(Span::styled(
|
||||
"Ultimate Training Modpack Menu",
|
||||
Style::default().fg(Color::LightRed),
|
||||
))))
|
||||
.style(Style::default().fg(Color::White))
|
||||
.highlight_style(Style::default().fg(Color::Yellow))
|
||||
.divider("|")
|
||||
.select(tab_selected_deduped);
|
||||
|
||||
let vertical_chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Length(2),
|
||||
Constraint::Max(10),
|
||||
Constraint::Length(2),
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.split(f.size());
|
||||
|
||||
// Prevent overflow by adding a length constraint of NX_TUI_WIDTH
|
||||
// Need to add a second constraint since the .expand_to_fill() method
|
||||
// is not publicly exposed, and the attribute defaults to true.
|
||||
// https://github.com/fdehau/tui-rs/blob/v0.19.0/src/layout.rs#L121
|
||||
let vertical_chunks: Vec<Rect> = vertical_chunks
|
||||
.iter()
|
||||
.map(|chunk| {
|
||||
Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Length(NX_TUI_WIDTH), // Width of the TUI terminal
|
||||
Constraint::Min(0), // Fill the remainder margin
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.split(*chunk)[0]
|
||||
})
|
||||
.collect();
|
||||
|
||||
let list_chunks = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints(
|
||||
(0..NUM_LISTS)
|
||||
.into_iter()
|
||||
.map(|_idx| Constraint::Percentage((100.0 / NUM_LISTS as f32) as u16))
|
||||
.collect::<Vec<Constraint>>()
|
||||
.as_ref(),
|
||||
)
|
||||
.split(vertical_chunks[1]);
|
||||
|
||||
f.render_widget(tabs, vertical_chunks[0]);
|
||||
|
||||
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!(),
|
||||
}
|
||||
}
|
||||
pub const NX_SUBMENU_ROWS: usize = 8;
|
||||
pub const NX_SUBMENU_COLUMNS: usize = 4;
|
||||
|
|
|
@ -1,209 +0,0 @@
|
|||
use tui::widgets::ListState;
|
||||
|
||||
pub struct MultiStatefulList<T> {
|
||||
pub lists: Vec<StatefulList<T>>,
|
||||
pub state: usize,
|
||||
pub total_len: usize,
|
||||
}
|
||||
|
||||
impl<T: Clone> MultiStatefulList<T> {
|
||||
pub fn selected_list_item(&mut self) -> &mut T {
|
||||
let (list_section, list_idx) = self.idx_to_list_idx(self.state);
|
||||
&mut self.lists[list_section].items[list_idx]
|
||||
}
|
||||
|
||||
pub fn idx_to_list_idx(&self, idx: usize) -> (usize, usize) {
|
||||
self.idx_to_list_idx_opt(idx).unwrap_or((0, 0))
|
||||
}
|
||||
|
||||
pub fn idx_to_list_idx_opt(&self, idx: usize) -> Option<(usize, usize)> {
|
||||
for list_section in 0..self.lists.len() {
|
||||
let list_section_min_idx =
|
||||
(self.total_len as f32 / self.lists.len() as f32).ceil() as usize * list_section;
|
||||
let list_section_max_idx = std::cmp::min(
|
||||
(self.total_len as f32 / self.lists.len() as f32).ceil() as usize
|
||||
* (list_section + 1),
|
||||
self.total_len,
|
||||
);
|
||||
if (list_section_min_idx..list_section_max_idx).contains(&idx) {
|
||||
return Some((list_section, idx - list_section_min_idx));
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn list_idx_to_idx(&self, list_idx: (usize, usize)) -> usize {
|
||||
let list_section = list_idx.0;
|
||||
let mut list_idx = list_idx.1;
|
||||
for list_section in 0..list_section {
|
||||
list_idx += self.lists[list_section].items.len();
|
||||
}
|
||||
list_idx
|
||||
}
|
||||
|
||||
pub fn with_items(items: Vec<T>, num_lists: usize) -> MultiStatefulList<T> {
|
||||
let lists = (0..num_lists)
|
||||
.map(|list_section| {
|
||||
// Try to evenly chunk
|
||||
let list_size = (items.len() as f32 / num_lists as f32).ceil() as usize;
|
||||
let list_section_min_idx = std::cmp::min(list_size * list_section, items.len());
|
||||
let list_section_max_idx =
|
||||
std::cmp::min(list_size * (list_section + 1), items.len());
|
||||
let mut state = ListState::default();
|
||||
if list_section == 0 {
|
||||
// Enforce state as first of list
|
||||
state.select(Some(0));
|
||||
}
|
||||
StatefulList {
|
||||
state,
|
||||
items: items[list_section_min_idx..list_section_max_idx].to_vec(),
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
let total_len = items.len();
|
||||
MultiStatefulList {
|
||||
lists,
|
||||
total_len,
|
||||
state: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn next(&mut self) {
|
||||
let (list_section, _) = self.idx_to_list_idx(self.state);
|
||||
let (next_list_section, next_list_idx) = self.idx_to_list_idx(self.state + 1);
|
||||
|
||||
if list_section != next_list_section {
|
||||
self.lists[list_section].unselect();
|
||||
}
|
||||
let state = if self.state + 1 >= self.total_len {
|
||||
(0, 0)
|
||||
} else {
|
||||
(next_list_section, next_list_idx)
|
||||
};
|
||||
|
||||
self.lists[state.0].state.select(Some(state.1));
|
||||
self.state = self.list_idx_to_idx(state);
|
||||
}
|
||||
|
||||
pub fn previous(&mut self) {
|
||||
let (list_section, _) = self.idx_to_list_idx(self.state);
|
||||
let mut last_list_section = self.lists.len() - 1;
|
||||
let mut last_list_size = self.lists[last_list_section].items.len();
|
||||
|
||||
while last_list_size == 0 {
|
||||
last_list_section -= 1;
|
||||
last_list_size = self.lists[last_list_section].items.len();
|
||||
}
|
||||
|
||||
let last_list_idx = last_list_size - 1;
|
||||
|
||||
self.lists[list_section].unselect();
|
||||
let state = if self.state == 0 {
|
||||
(last_list_section, last_list_idx)
|
||||
} else {
|
||||
let (prev_list_section, prev_list_idx) = self.idx_to_list_idx(self.state - 1);
|
||||
(prev_list_section, prev_list_idx)
|
||||
};
|
||||
|
||||
self.lists[state.0].state.select(Some(state.1));
|
||||
self.state = self.list_idx_to_idx(state);
|
||||
}
|
||||
|
||||
pub fn next_list(&mut self) {
|
||||
let (list_section, list_idx) = self.idx_to_list_idx(self.state);
|
||||
let mut next_list_section = (list_section + 1) % self.lists.len();
|
||||
let mut next_list_len = self.lists[next_list_section].items.len();
|
||||
while next_list_len == 0 {
|
||||
next_list_section = (next_list_section + 1) % self.lists.len();
|
||||
next_list_len = self.lists[next_list_section].items.len();
|
||||
}
|
||||
let next_list_idx = if list_idx > next_list_len - 1 {
|
||||
next_list_len - 1
|
||||
} else {
|
||||
list_idx
|
||||
};
|
||||
|
||||
if list_section != next_list_section {
|
||||
self.lists[list_section].unselect();
|
||||
}
|
||||
let state = (next_list_section, next_list_idx);
|
||||
|
||||
self.lists[state.0].state.select(Some(state.1));
|
||||
self.state = self.list_idx_to_idx(state);
|
||||
}
|
||||
|
||||
pub fn previous_list(&mut self) {
|
||||
let (list_section, list_idx) = self.idx_to_list_idx(self.state);
|
||||
let mut prev_list_section = if list_section == 0 {
|
||||
self.lists.len() - 1
|
||||
} else {
|
||||
list_section - 1
|
||||
};
|
||||
|
||||
let mut prev_list_len = self.lists[prev_list_section].items.len();
|
||||
while prev_list_len == 0 {
|
||||
prev_list_section -= 1;
|
||||
prev_list_len = self.lists[prev_list_section].items.len();
|
||||
}
|
||||
let prev_list_idx = if list_idx > prev_list_len - 1 {
|
||||
prev_list_len - 1
|
||||
} else {
|
||||
list_idx
|
||||
};
|
||||
|
||||
if list_section != prev_list_section {
|
||||
self.lists[list_section].unselect();
|
||||
}
|
||||
let state = (prev_list_section, prev_list_idx);
|
||||
|
||||
self.lists[state.0].state.select(Some(state.1));
|
||||
self.state = self.list_idx_to_idx(state);
|
||||
}
|
||||
}
|
||||
|
||||
pub struct StatefulList<T> {
|
||||
pub state: ListState,
|
||||
pub items: Vec<T>,
|
||||
}
|
||||
|
||||
impl<T> StatefulList<T> {
|
||||
pub fn with_items(items: Vec<T>) -> StatefulList<T> {
|
||||
let mut state = ListState::default();
|
||||
// Enforce state as first of list
|
||||
state.select(Some(0));
|
||||
StatefulList { state, items }
|
||||
}
|
||||
|
||||
pub fn next(&mut self) {
|
||||
let i = match self.state.selected() {
|
||||
Some(i) => {
|
||||
if i >= self.items.len() - 1 {
|
||||
0
|
||||
} else {
|
||||
i + 1
|
||||
}
|
||||
}
|
||||
None => 0,
|
||||
};
|
||||
self.state.select(Some(i));
|
||||
}
|
||||
|
||||
pub fn previous(&mut self) {
|
||||
let i = match self.state.selected() {
|
||||
Some(i) => {
|
||||
if i == 0 {
|
||||
self.items.len() - 1
|
||||
} else {
|
||||
i - 1
|
||||
}
|
||||
}
|
||||
None => 0,
|
||||
};
|
||||
self.state.select(Some(i));
|
||||
}
|
||||
|
||||
pub fn unselect(&mut self) {
|
||||
self.state.select(None);
|
||||
}
|
||||
}
|
|
@ -1,360 +0,0 @@
|
|||
#[cfg(feature = "has_terminal")]
|
||||
use crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
|
||||
use std::error::Error;
|
||||
#[cfg(feature = "has_terminal")]
|
||||
use std::{
|
||||
io,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
#[cfg(feature = "has_terminal")]
|
||||
use tui::backend::CrosstermBackend;
|
||||
use tui::Terminal;
|
||||
|
||||
use training_mod_consts::*;
|
||||
|
||||
fn test_backend_setup(
|
||||
ui_menu: UiMenu,
|
||||
menu_defaults: (UiMenu, String),
|
||||
) -> Result<
|
||||
(
|
||||
Terminal<training_mod_tui::TestBackend>,
|
||||
training_mod_tui::App,
|
||||
),
|
||||
Box<dyn Error>,
|
||||
> {
|
||||
let app = training_mod_tui::App::new(ui_menu, menu_defaults);
|
||||
let backend = tui::backend::TestBackend::new(120, 15);
|
||||
let terminal = Terminal::new(backend)?;
|
||||
let mut state = tui::widgets::ListState::default();
|
||||
state.select(Some(1));
|
||||
|
||||
Ok((terminal, app))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_set_airdodge() -> 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 Section
|
||||
app.next_tab();
|
||||
// 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 {
|
||||
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 Section
|
||||
app.next_tab();
|
||||
// Enter Mash Toggles
|
||||
app.on_a();
|
||||
// Set Mash Airdodge
|
||||
app.on_a();
|
||||
// Return to submenu selection
|
||||
app.on_b();
|
||||
// Save Defaults
|
||||
app.save_defaults();
|
||||
// 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.reset_current_submenu();
|
||||
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 Section
|
||||
app.next_tab();
|
||||
// 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.save_defaults();
|
||||
// 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.reset_all_submenus();
|
||||
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(())
|
||||
}
|
||||
|
||||
fn _get_frame_buffer(
|
||||
mut terminal: Terminal<training_mod_tui::TestBackend>,
|
||||
mut app: training_mod_tui::App,
|
||||
) -> Result<String, Box<dyn Error>> {
|
||||
let frame_res = terminal.draw(|f| training_mod_tui::ui(f, &mut app))?;
|
||||
let mut full_frame_buffer = String::new();
|
||||
for (i, cell) in frame_res.buffer.content().iter().enumerate() {
|
||||
full_frame_buffer.push_str(&cell.symbol);
|
||||
if i % frame_res.area.width as usize == frame_res.area.width as usize - 1 {
|
||||
full_frame_buffer.push_str("\n");
|
||||
}
|
||||
}
|
||||
full_frame_buffer.push_str("\n");
|
||||
|
||||
Ok(full_frame_buffer)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_toggle_naming() -> 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 (mut terminal, mut app) = test_backend_setup(menu, menu_defaults)?;
|
||||
// Enter Mash Section
|
||||
app.next_tab();
|
||||
// Enter Mash Toggles
|
||||
app.on_a();
|
||||
// Set Mash Airdodge
|
||||
app.on_a();
|
||||
|
||||
let frame_buffer = _get_frame_buffer(terminal, app)?;
|
||||
assert!(frame_buffer.contains("Airdodge"));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
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 = 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, menu_defaults)?;
|
||||
if inputs.is_some() {
|
||||
inputs
|
||||
.unwrap()
|
||||
.split(",")
|
||||
.for_each(|input| match input.to_uppercase().as_str() {
|
||||
"X" => app.save_defaults(),
|
||||
"Y" => app.reset_current_submenu(),
|
||||
"Z" => app.reset_all_submenus(),
|
||||
"L" => app.previous_tab(),
|
||||
"R" => app.next_tab(),
|
||||
"A" => app.on_a(),
|
||||
"B" => app.on_b(),
|
||||
"UP" => app.on_up(),
|
||||
"DOWN" => app.on_down(),
|
||||
"LEFT" => app.on_left(),
|
||||
"RIGHT" => app.on_right(),
|
||||
_ => {}
|
||||
})
|
||||
}
|
||||
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);
|
||||
if i % frame_res.area.width as usize == frame_res.area.width as usize - 1 {
|
||||
println!();
|
||||
}
|
||||
}
|
||||
println!();
|
||||
|
||||
println!("Menu:\n{menu_json}");
|
||||
}
|
||||
|
||||
#[cfg(feature = "has_terminal")]
|
||||
{
|
||||
let app = training_mod_tui::App::new(menu, menu_defaults);
|
||||
|
||||
// setup terminal
|
||||
enable_raw_mode()?;
|
||||
let mut stdout = io::stdout();
|
||||
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
|
||||
let backend = CrosstermBackend::new(stdout);
|
||||
let mut terminal = Terminal::new(backend)?;
|
||||
|
||||
let tick_rate = Duration::from_millis(250);
|
||||
let res = run_app(&mut terminal, app, tick_rate);
|
||||
|
||||
// restore terminal
|
||||
disable_raw_mode()?;
|
||||
execute!(
|
||||
terminal.backend_mut(),
|
||||
LeaveAlternateScreen,
|
||||
DisableMouseCapture
|
||||
)?;
|
||||
terminal.show_cursor()?;
|
||||
|
||||
if let Err(err) = res {
|
||||
println!("{:?}", err)
|
||||
} else {
|
||||
println!("JSONs: {:#?}", res.as_ref().unwrap());
|
||||
unsafe {
|
||||
let menu = serde_json::from_str::<MenuJsonStruct>(&res.as_ref().unwrap()).unwrap();
|
||||
println!("menu: {:#?}", menu);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(feature = "has_terminal")]
|
||||
fn run_app<B: tui::backend::Backend>(
|
||||
terminal: &mut Terminal<B>,
|
||||
mut app: training_mod_tui::App,
|
||||
tick_rate: Duration,
|
||||
) -> io::Result<String> {
|
||||
let mut last_tick = Instant::now();
|
||||
loop {
|
||||
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())
|
||||
.unwrap_or_else(|| Duration::from_secs(0));
|
||||
|
||||
if crossterm::event::poll(timeout)? {
|
||||
if let Event::Key(key) = event::read()? {
|
||||
match key.code {
|
||||
KeyCode::Char('q') => return Ok(menu_json),
|
||||
KeyCode::Char('x') => app.save_defaults(),
|
||||
KeyCode::Char('p') => app.reset_current_submenu(),
|
||||
KeyCode::Char('o') => app.reset_all_submenus(),
|
||||
KeyCode::Char('r') => app.next_tab(),
|
||||
KeyCode::Char('l') => app.previous_tab(),
|
||||
KeyCode::Left => app.on_left(),
|
||||
KeyCode::Right => app.on_right(),
|
||||
KeyCode::Down => app.on_down(),
|
||||
KeyCode::Up => app.on_up(),
|
||||
KeyCode::Enter => app.on_a(),
|
||||
KeyCode::Backspace => app.on_b(),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
if last_tick.elapsed() >= tick_rate {
|
||||
last_tick = Instant::now();
|
||||
}
|
||||
}
|
||||
}
|
6
training_mod_tui/src/structures/mod.rs
Normal file
6
training_mod_tui/src/structures/mod.rs
Normal file
|
@ -0,0 +1,6 @@
|
|||
mod stateful_list;
|
||||
mod stateful_slider;
|
||||
mod stateful_table;
|
||||
pub use stateful_list::*;
|
||||
pub use stateful_slider::*;
|
||||
pub use stateful_table::*;
|
124
training_mod_tui/src/structures/stateful_list.rs
Normal file
124
training_mod_tui/src/structures/stateful_list.rs
Normal file
|
@ -0,0 +1,124 @@
|
|||
use ratatui::widgets::ListState;
|
||||
use serde::ser::{Serialize, SerializeSeq, Serializer};
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Clone)]
|
||||
pub struct StatefulList<T: Serialize> {
|
||||
pub state: ListState,
|
||||
pub items: Vec<T>,
|
||||
}
|
||||
|
||||
impl<T: Serialize> IntoIterator for StatefulList<T> {
|
||||
type Item = T;
|
||||
type IntoIter = std::vec::IntoIter<Self::Item>;
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.items.into_iter()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Serialize> StatefulList<T> {
|
||||
pub fn new() -> StatefulList<T> {
|
||||
StatefulList {
|
||||
state: ListState::default(),
|
||||
items: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_items(items: Vec<T>) -> StatefulList<T> {
|
||||
let mut state = ListState::default();
|
||||
// Enforce state as first of list
|
||||
state.select(Some(0));
|
||||
StatefulList { state, items }
|
||||
}
|
||||
|
||||
pub fn push(&mut self, item: T) {
|
||||
self.items.push(item);
|
||||
}
|
||||
|
||||
pub fn next(&mut self) {
|
||||
let i = match self.state.selected() {
|
||||
Some(i) => {
|
||||
if i >= self.items.len() - 1 {
|
||||
0
|
||||
} else {
|
||||
i + 1
|
||||
}
|
||||
}
|
||||
None => 0,
|
||||
};
|
||||
self.state.select(Some(i));
|
||||
}
|
||||
|
||||
pub fn previous(&mut self) {
|
||||
let i = match self.state.selected() {
|
||||
Some(i) => {
|
||||
if i == 0 {
|
||||
self.items.len() - 1
|
||||
} else {
|
||||
i - 1
|
||||
}
|
||||
}
|
||||
None => 0,
|
||||
};
|
||||
self.state.select(Some(i));
|
||||
}
|
||||
|
||||
pub fn unselect(&mut self) {
|
||||
self.state.select(None);
|
||||
}
|
||||
|
||||
pub fn get_selected(&mut self) -> Option<&mut T> {
|
||||
if let Some(selected_index) = self.state.selected() {
|
||||
Some(&mut self.items[selected_index])
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_before_selected(&mut self) -> Option<&mut T> {
|
||||
let len = self.items.len();
|
||||
if let Some(selected_index) = self.state.selected() {
|
||||
if selected_index == 0 {
|
||||
Some(&mut self.items[len - 1])
|
||||
} else {
|
||||
Some(&mut self.items[selected_index - 1])
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_after_selected(&mut self) -> Option<&mut T> {
|
||||
let len = self.items.len();
|
||||
if let Some(selected_index) = self.state.selected() {
|
||||
if selected_index == len - 1 {
|
||||
Some(&mut self.items[0])
|
||||
} else {
|
||||
Some(&mut self.items[selected_index + 1])
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Serialize> Serialize for StatefulList<T> {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
let mut seq = serializer.serialize_seq(Some(self.items.len()))?;
|
||||
for e in self.items.iter() {
|
||||
seq.serialize_element(e)?;
|
||||
}
|
||||
seq.end()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Serialize> StatefulList<T> {
|
||||
pub fn iter(&self) -> impl Iterator<Item = &T> + '_ {
|
||||
self.items.iter()
|
||||
}
|
||||
pub fn iter_mut(&mut self) -> std::slice::IterMut<T> {
|
||||
self.items.iter_mut()
|
||||
}
|
||||
}
|
146
training_mod_tui/src/structures/stateful_slider.rs
Normal file
146
training_mod_tui/src/structures/stateful_slider.rs
Normal file
|
@ -0,0 +1,146 @@
|
|||
use serde::{Serialize, Serializer};
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Debug)]
|
||||
pub enum SliderState {
|
||||
LowerHover,
|
||||
UpperHover,
|
||||
LowerSelected,
|
||||
UpperSelected,
|
||||
None,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Debug)]
|
||||
pub struct StatefulSlider {
|
||||
pub state: SliderState,
|
||||
pub lower: u32,
|
||||
pub upper: u32,
|
||||
pub min: u32,
|
||||
pub max: u32,
|
||||
pub incr_amount_slow: u32,
|
||||
pub incr_amount_fast: u32,
|
||||
}
|
||||
|
||||
impl StatefulSlider {
|
||||
pub fn new() -> StatefulSlider {
|
||||
StatefulSlider {
|
||||
state: SliderState::LowerHover,
|
||||
lower: 0,
|
||||
upper: 150,
|
||||
min: 0,
|
||||
max: 150,
|
||||
incr_amount_slow: 1,
|
||||
incr_amount_fast: 10,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn increment_selected_slow(&mut self) {
|
||||
match self.state {
|
||||
SliderState::LowerSelected => {
|
||||
self.lower = self
|
||||
.lower
|
||||
.saturating_add(self.incr_amount_slow)
|
||||
.min(self.upper); // Don't allow lower > upper
|
||||
}
|
||||
SliderState::UpperSelected => {
|
||||
self.upper = self
|
||||
.upper
|
||||
.saturating_add(self.incr_amount_slow)
|
||||
.min(self.max); // Don't allow upper > max
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn increment_selected_fast(&mut self) {
|
||||
match self.state {
|
||||
SliderState::LowerSelected => {
|
||||
self.lower = self
|
||||
.lower
|
||||
.saturating_add(self.incr_amount_fast)
|
||||
.min(self.upper); // Don't allow lower > upper
|
||||
}
|
||||
SliderState::UpperSelected => {
|
||||
self.upper = self
|
||||
.upper
|
||||
.saturating_add(self.incr_amount_fast)
|
||||
.min(self.max); // Don't allow upper > max
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn decrement_selected_slow(&mut self) {
|
||||
match self.state {
|
||||
SliderState::LowerSelected => {
|
||||
self.lower = self
|
||||
.lower
|
||||
.saturating_sub(self.incr_amount_slow)
|
||||
.max(self.min); // Don't allow lower < min
|
||||
}
|
||||
SliderState::UpperSelected => {
|
||||
self.upper = self
|
||||
.upper
|
||||
.saturating_sub(self.incr_amount_slow)
|
||||
.max(self.lower); // Don't allow upper < lower
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn decrement_selected_fast(&mut self) {
|
||||
match self.state {
|
||||
SliderState::LowerSelected => {
|
||||
self.lower = self
|
||||
.lower
|
||||
.saturating_sub(self.incr_amount_fast)
|
||||
.max(self.min); // Don't allow lower < min
|
||||
}
|
||||
SliderState::UpperSelected => {
|
||||
self.upper = self
|
||||
.upper
|
||||
.saturating_sub(self.incr_amount_fast)
|
||||
.max(self.lower); // Don't allow upper < lower
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn select_deselect(&mut self) {
|
||||
self.state = match self.state {
|
||||
SliderState::LowerHover => SliderState::LowerSelected,
|
||||
SliderState::LowerSelected => SliderState::LowerHover,
|
||||
SliderState::UpperHover => SliderState::UpperSelected,
|
||||
SliderState::UpperSelected => SliderState::UpperHover,
|
||||
SliderState::None => SliderState::None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn deselect(&mut self) {
|
||||
self.state = match self.state {
|
||||
SliderState::LowerSelected => SliderState::LowerHover,
|
||||
SliderState::UpperSelected => SliderState::UpperHover,
|
||||
_ => self.state,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn switch_hover(&mut self) {
|
||||
self.state = match self.state {
|
||||
SliderState::LowerHover => SliderState::UpperHover,
|
||||
SliderState::UpperHover => SliderState::LowerHover,
|
||||
_ => self.state,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_handle_selected(&mut self) -> bool {
|
||||
self.state == SliderState::LowerSelected || self.state == SliderState::UpperSelected
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for StatefulSlider {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
[self.lower, self.upper].serialize(serializer)
|
||||
}
|
||||
}
|
274
training_mod_tui/src/structures/stateful_table.rs
Normal file
274
training_mod_tui/src/structures/stateful_table.rs
Normal file
|
@ -0,0 +1,274 @@
|
|||
use ratatui::widgets::*;
|
||||
use serde::{Serialize, Serializer};
|
||||
|
||||
/// Allows a snake-filled table of arbitrary size
|
||||
/// The final row does not need to be filled
|
||||
/// [ a , b , c , d ]
|
||||
/// [ e, f, g, h, i ]
|
||||
/// [ j, k ]
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Clone)]
|
||||
pub struct StatefulTable<T: Clone + Serialize> {
|
||||
pub state: TableState,
|
||||
pub items: Vec<Vec<Option<T>>>,
|
||||
pub rows: usize,
|
||||
pub cols: usize,
|
||||
}
|
||||
|
||||
// Size-related functions
|
||||
impl<T: Clone + Serialize> StatefulTable<T> {
|
||||
pub fn len(&self) -> usize {
|
||||
self.items
|
||||
.iter()
|
||||
.map(|row| {
|
||||
row.iter()
|
||||
.map(|item| if item.is_some() { 1 } else { 0 })
|
||||
.sum::<usize>()
|
||||
})
|
||||
.sum()
|
||||
}
|
||||
pub fn full_len(&self) -> usize {
|
||||
self.rows * self.cols
|
||||
}
|
||||
pub fn as_vec(&self) -> Vec<T> {
|
||||
let mut v = Vec::with_capacity(self.len());
|
||||
for row in self.items.iter() {
|
||||
for item in row.iter() {
|
||||
if let Some(i) = item {
|
||||
v.push(i.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
v
|
||||
}
|
||||
}
|
||||
|
||||
// Initalization Functions
|
||||
impl<T: Clone + Serialize> StatefulTable<T> {
|
||||
pub fn new(rows: usize, cols: usize) -> Self {
|
||||
Self {
|
||||
state: TableState::default().with_selected(Some(TableSelection::default())),
|
||||
items: vec![vec![None; cols]; rows],
|
||||
rows: rows,
|
||||
cols: cols,
|
||||
}
|
||||
}
|
||||
pub fn with_items(rows: usize, cols: usize, v: Vec<T>) -> Self {
|
||||
let mut table: Self = Self::new(rows, cols);
|
||||
if v.len() > rows * cols {
|
||||
panic!(
|
||||
"Cannot create StatefulTable; too many items for size {}x{}: {}",
|
||||
rows,
|
||||
cols,
|
||||
v.len()
|
||||
);
|
||||
} else {
|
||||
for (i, item) in v.iter().enumerate() {
|
||||
table.items[i.div_euclid(cols)][i.rem_euclid(cols)] = Some(item.clone());
|
||||
}
|
||||
table
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// State Functions
|
||||
impl<T: Clone + Serialize> StatefulTable<T> {
|
||||
pub fn select(&mut self, row: usize, col: usize) {
|
||||
assert!(col < self.cols);
|
||||
assert!(row < self.rows);
|
||||
self.state.select(Some(TableSelection::Cell { row, col }));
|
||||
}
|
||||
|
||||
pub fn get_selected(&mut self) -> Option<&mut T> {
|
||||
self.items[self.state.selected_row().unwrap()][self.state.selected_col().unwrap()].as_mut()
|
||||
}
|
||||
|
||||
pub fn get(&self, row: usize, column: usize) -> Option<&T> {
|
||||
if row >= self.rows || column >= self.cols {
|
||||
None
|
||||
} else {
|
||||
self.items[row][column].as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_mut(&mut self, row: usize, column: usize) -> Option<&mut T> {
|
||||
if row >= self.rows || column >= self.cols {
|
||||
None
|
||||
} else {
|
||||
self.items[row][column].as_mut()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_by_idx(&self, idx: usize) -> Option<&T> {
|
||||
let row = idx.div_euclid(self.cols);
|
||||
let col = idx.rem_euclid(self.cols);
|
||||
self.get(row, col)
|
||||
}
|
||||
pub fn get_by_idx_mut(&mut self, idx: usize) -> Option<&mut T> {
|
||||
let row = idx.div_euclid(self.cols);
|
||||
let col = idx.rem_euclid(self.cols);
|
||||
self.get_mut(row, col)
|
||||
}
|
||||
|
||||
pub fn next_row(&mut self) {
|
||||
let next_row = match self.state.selected_row() {
|
||||
Some(row) => {
|
||||
if row == self.items.len() - 1 {
|
||||
0
|
||||
} else {
|
||||
row + 1
|
||||
}
|
||||
}
|
||||
|
||||
None => 0,
|
||||
};
|
||||
self.state.select_row(Some(next_row));
|
||||
}
|
||||
|
||||
pub fn next_row_checked(&mut self) {
|
||||
self.next_row();
|
||||
if self.get_selected().is_none() {
|
||||
self.next_row_checked();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn prev_row(&mut self) {
|
||||
let prev_row = match self.state.selected_row() {
|
||||
Some(row) => {
|
||||
if row == 0 {
|
||||
self.items.len() - 1
|
||||
} else {
|
||||
row - 1
|
||||
}
|
||||
}
|
||||
None => 0,
|
||||
};
|
||||
|
||||
self.state.select_row(Some(prev_row));
|
||||
}
|
||||
|
||||
pub fn prev_row_checked(&mut self) {
|
||||
self.prev_row();
|
||||
if self.get_selected().is_none() {
|
||||
self.prev_row_checked();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn next_col(&mut self) {
|
||||
// Assumes that all rows are the same width
|
||||
let next_col = match self.state.selected_col() {
|
||||
Some(col) => {
|
||||
if col == self.items[0].len() - 1 {
|
||||
0
|
||||
} else {
|
||||
col + 1
|
||||
}
|
||||
}
|
||||
None => 0,
|
||||
};
|
||||
self.state.select_col(Some(next_col));
|
||||
}
|
||||
|
||||
pub fn next_col_checked(&mut self) {
|
||||
self.next_col();
|
||||
if self.get_selected().is_none() {
|
||||
self.next_col_checked();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn prev_col(&mut self) {
|
||||
let prev_col = match self.state.selected_col() {
|
||||
Some(col) => {
|
||||
if col == 0 {
|
||||
self.items[0].len() - 1
|
||||
} else {
|
||||
col - 1
|
||||
}
|
||||
}
|
||||
None => 0,
|
||||
};
|
||||
self.state.select_col(Some(prev_col));
|
||||
}
|
||||
|
||||
pub fn prev_col_checked(&mut self) {
|
||||
self.prev_col();
|
||||
self.carriage_return();
|
||||
}
|
||||
|
||||
/// If the selected cell is None, move selection to the left until you get Some.
|
||||
/// No-op if the selected cell is Some.
|
||||
/// For example, a 2x3 table with 4 elements would shift the selection from 1,2 to 1,0
|
||||
///
|
||||
/// [ a , b , c ]
|
||||
/// [ d , e , [ ]]
|
||||
///
|
||||
/// |
|
||||
/// V
|
||||
///
|
||||
/// [ a , b , c ]
|
||||
/// [[d], , ]
|
||||
pub fn carriage_return(&mut self) {
|
||||
assert!(
|
||||
self.items[self.state.selected_row().unwrap()]
|
||||
.iter()
|
||||
.any(|x| x.is_some()),
|
||||
"Carriage return called on an empty row!"
|
||||
);
|
||||
if self.get_selected().is_none() {
|
||||
self.prev_col();
|
||||
self.carriage_return();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Clone + Serialize> Serialize for StatefulTable<T> {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
let flat: Vec<T> = self.as_vec();
|
||||
flat.serialize(serializer)
|
||||
}
|
||||
}
|
||||
|
||||
// Implement .iter() for StatefulTable
|
||||
pub struct StatefulTableIterator<'a, T: Clone + Serialize> {
|
||||
stateful_table: &'a StatefulTable<T>,
|
||||
index: usize,
|
||||
}
|
||||
|
||||
impl<'a, T: Clone + Serialize> Iterator for StatefulTableIterator<'a, T> {
|
||||
type Item = &'a T;
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
self.index += 1;
|
||||
self.stateful_table.get_by_idx(self.index - 1)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Clone + Serialize> StatefulTable<T> {
|
||||
pub fn iter(&self) -> StatefulTableIterator<T> {
|
||||
StatefulTableIterator {
|
||||
stateful_table: self,
|
||||
index: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct StatefulTableIteratorMut<'a, T: Clone + Serialize> {
|
||||
inner: std::iter::Flatten<std::slice::IterMut<'a, Vec<Option<T>>>>,
|
||||
}
|
||||
|
||||
impl<'a, T: Clone + Serialize> Iterator for StatefulTableIteratorMut<'a, T> {
|
||||
type Item = &'a mut Option<T>;
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
self.inner.next()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T: Clone + Serialize + 'a> StatefulTable<T> {
|
||||
pub fn iter_mut(&'a mut self) -> StatefulTableIteratorMut<T> {
|
||||
StatefulTableIteratorMut {
|
||||
inner: self.items.iter_mut().flatten(),
|
||||
}
|
||||
}
|
||||
}
|
182
training_mod_tui/tests/test_stateful_list.rs
Normal file
182
training_mod_tui/tests/test_stateful_list.rs
Normal file
|
@ -0,0 +1,182 @@
|
|||
use ratatui::widgets::ListState;
|
||||
use training_mod_tui_2::StatefulList;
|
||||
|
||||
fn initialize_list(selected: Option<usize>) -> StatefulList<u8> {
|
||||
StatefulList {
|
||||
state: initialize_state(selected),
|
||||
items: vec![10, 20, 30, 40],
|
||||
}
|
||||
}
|
||||
|
||||
fn initialize_state(selected: Option<usize>) -> ListState {
|
||||
let mut state = ListState::default();
|
||||
state.select(selected);
|
||||
state
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stateful_list_test_new() {
|
||||
let l = initialize_list(None);
|
||||
assert_eq!(
|
||||
l,
|
||||
StatefulList {
|
||||
state: ListState::default(),
|
||||
items: vec![10, 20, 30, 40],
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stateful_list_with_items() {
|
||||
let l = initialize_list(Some(0));
|
||||
let m = StatefulList::<u8>::with_items(vec![10, 20, 30, 40]);
|
||||
assert_eq!(l, m);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stateful_list_next() {
|
||||
let mut l = initialize_list(Some(0));
|
||||
let mut state = ListState::default();
|
||||
state.select(Some(0));
|
||||
assert_eq!(l.state, state);
|
||||
assert_eq!(l.get_selected(), Some(&mut 10));
|
||||
l.next();
|
||||
state.select(Some(1));
|
||||
assert_eq!(l.state, state);
|
||||
assert_eq!(l.get_selected(), Some(&mut 20));
|
||||
l.next();
|
||||
state.select(Some(2));
|
||||
assert_eq!(l.state, state);
|
||||
assert_eq!(l.get_selected(), Some(&mut 30));
|
||||
l.next();
|
||||
state.select(Some(3));
|
||||
assert_eq!(l.state, state);
|
||||
assert_eq!(l.get_selected(), Some(&mut 40));
|
||||
l.next();
|
||||
state.select(Some(0));
|
||||
assert_eq!(l.state, state);
|
||||
assert_eq!(l.get_selected(), Some(&mut 10));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stateful_list_prev() {
|
||||
let mut l = initialize_list(Some(0));
|
||||
let mut state = ListState::default();
|
||||
state.select(Some(0));
|
||||
assert_eq!(l.state, state);
|
||||
assert_eq!(l.get_selected(), Some(&mut 10));
|
||||
l.previous();
|
||||
state.select(Some(3));
|
||||
assert_eq!(l.state, state);
|
||||
assert_eq!(l.get_selected(), Some(&mut 40));
|
||||
l.previous();
|
||||
state.select(Some(2));
|
||||
assert_eq!(l.state, state);
|
||||
assert_eq!(l.get_selected(), Some(&mut 30));
|
||||
l.previous();
|
||||
state.select(Some(1));
|
||||
assert_eq!(l.state, state);
|
||||
assert_eq!(l.get_selected(), Some(&mut 20));
|
||||
l.previous();
|
||||
state.select(Some(0));
|
||||
assert_eq!(l.state, state);
|
||||
assert_eq!(l.get_selected(), Some(&mut 10));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stateful_list_unselect() {
|
||||
let mut l = initialize_list(Some(0));
|
||||
let state = ListState::default();
|
||||
l.unselect();
|
||||
assert_eq!(l.state, state);
|
||||
l.unselect();
|
||||
assert_eq!(l.state, state);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stateful_list_get_selected() {
|
||||
let mut l = initialize_list(None);
|
||||
assert_eq!(l.get_selected(), None);
|
||||
l.state.select(Some(0));
|
||||
assert_eq!(l.get_selected(), Some(&mut 10));
|
||||
l.state.select(Some(1));
|
||||
assert_eq!(l.get_selected(), Some(&mut 20));
|
||||
l.state.select(Some(2));
|
||||
assert_eq!(l.get_selected(), Some(&mut 30));
|
||||
l.state.select(Some(3));
|
||||
assert_eq!(l.get_selected(), Some(&mut 40));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stateful_list_get_before_selected() {
|
||||
let mut l = initialize_list(None);
|
||||
assert_eq!(l.get_before_selected(), None);
|
||||
l.state.select(Some(0));
|
||||
assert_eq!(l.get_before_selected(), Some(&mut 40));
|
||||
l.state.select(Some(1));
|
||||
assert_eq!(l.get_before_selected(), Some(&mut 10));
|
||||
l.state.select(Some(2));
|
||||
assert_eq!(l.get_before_selected(), Some(&mut 20));
|
||||
l.state.select(Some(3));
|
||||
assert_eq!(l.get_before_selected(), Some(&mut 30));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stateful_list_get_after_selected() {
|
||||
let mut l = initialize_list(None);
|
||||
assert_eq!(l.get_after_selected(), None);
|
||||
l.state.select(Some(0));
|
||||
assert_eq!(l.get_after_selected(), Some(&mut 20));
|
||||
l.state.select(Some(1));
|
||||
assert_eq!(l.get_after_selected(), Some(&mut 30));
|
||||
l.state.select(Some(2));
|
||||
assert_eq!(l.get_after_selected(), Some(&mut 40));
|
||||
l.state.select(Some(3));
|
||||
assert_eq!(l.get_after_selected(), Some(&mut 10));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stateful_list_serialize() {
|
||||
let l = initialize_list(Some(2));
|
||||
let l_json = serde_json::to_string(&l).unwrap();
|
||||
assert_eq!(&l_json, "[10,20,30,40]");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stateful_list_iter() {
|
||||
let l = initialize_list(Some(2));
|
||||
let mut l_iter = l.iter();
|
||||
assert_eq!(l_iter.next(), Some(&10));
|
||||
assert_eq!(l_iter.next(), Some(&20));
|
||||
assert_eq!(l_iter.next(), Some(&30));
|
||||
assert_eq!(l_iter.next(), Some(&40));
|
||||
assert_eq!(l_iter.next(), None);
|
||||
assert_eq!(l_iter.next(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stateful_list_iter_mut() {
|
||||
let mut l = initialize_list(Some(2));
|
||||
let mut l_iter_mut = l.iter_mut();
|
||||
assert_eq!(l_iter_mut.next(), Some(&mut 10));
|
||||
assert_eq!(l_iter_mut.next(), Some(&mut 20));
|
||||
assert_eq!(l_iter_mut.next(), Some(&mut 30));
|
||||
assert_eq!(l_iter_mut.next(), Some(&mut 40));
|
||||
assert_eq!(l_iter_mut.next(), None);
|
||||
assert_eq!(l_iter_mut.next(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stateful_list_push() {
|
||||
let mut l = initialize_list(None);
|
||||
l.push(5);
|
||||
l.push(6);
|
||||
l.push(7);
|
||||
assert_eq!(
|
||||
l,
|
||||
StatefulList {
|
||||
state: ListState::default(),
|
||||
items: vec![10, 20, 30, 40, 5, 6, 7],
|
||||
}
|
||||
);
|
||||
}
|
288
training_mod_tui/tests/test_stateful_slider.rs
Normal file
288
training_mod_tui/tests/test_stateful_slider.rs
Normal file
|
@ -0,0 +1,288 @@
|
|||
use training_mod_tui_2::{SliderState, StatefulSlider};
|
||||
|
||||
fn initialize_slider(state: SliderState) -> StatefulSlider {
|
||||
StatefulSlider {
|
||||
state: state,
|
||||
lower: 0,
|
||||
upper: 150,
|
||||
min: 0,
|
||||
max: 150,
|
||||
incr_amount_slow: 1,
|
||||
incr_amount_fast: 10,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stateful_slider_new() {
|
||||
let s = initialize_slider(SliderState::LowerHover);
|
||||
assert_eq!(s, StatefulSlider::new());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stateful_slider_increment_selected_slow() {
|
||||
let mut s = initialize_slider(SliderState::LowerHover);
|
||||
s.upper = 50;
|
||||
|
||||
// Check LowerHover: no increment
|
||||
s.increment_selected_slow();
|
||||
assert_eq!(s.lower, 0);
|
||||
|
||||
// Check LowerSelected: lower increments
|
||||
s.state = SliderState::LowerSelected;
|
||||
s.increment_selected_slow();
|
||||
assert_eq!(s.lower, 1);
|
||||
|
||||
// Check LowerSelected: lower can't go above upper
|
||||
s.lower = s.upper;
|
||||
s.increment_selected_slow();
|
||||
assert_eq!(s.lower, s.upper);
|
||||
|
||||
// Check UpperHover: no increment
|
||||
s.lower = 5;
|
||||
s.upper = 50;
|
||||
s.state = SliderState::UpperHover;
|
||||
s.increment_selected_slow();
|
||||
assert_eq!(s.lower, 5);
|
||||
assert_eq!(s.upper, 50);
|
||||
|
||||
// Check UpperSelected: upper increments
|
||||
s.state = SliderState::UpperSelected;
|
||||
s.increment_selected_slow();
|
||||
assert_eq!(s.lower, 5);
|
||||
assert_eq!(s.upper, 51);
|
||||
|
||||
// Check UpperSelected: upper can't go above max
|
||||
s.upper = s.max;
|
||||
s.increment_selected_slow();
|
||||
assert_eq!(s.lower, 5);
|
||||
assert_eq!(s.upper, s.max);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stateful_slider_increment_selected_fast() {
|
||||
let mut s = initialize_slider(SliderState::LowerHover);
|
||||
s.upper = 50;
|
||||
|
||||
// Check LowerHover: no increment
|
||||
s.increment_selected_fast();
|
||||
assert_eq!(s.lower, 0);
|
||||
|
||||
// Check LowerSelected: lower increments
|
||||
s.state = SliderState::LowerSelected;
|
||||
s.increment_selected_fast();
|
||||
assert_eq!(s.lower, 10);
|
||||
|
||||
// Check LowerSelected: lower can't go above upper
|
||||
s.lower = s.upper;
|
||||
s.increment_selected_fast();
|
||||
assert_eq!(s.lower, s.upper);
|
||||
|
||||
// Check UpperHover: no increment
|
||||
s.lower = 5;
|
||||
s.upper = 50;
|
||||
s.state = SliderState::UpperHover;
|
||||
s.increment_selected_fast();
|
||||
assert_eq!(s.lower, 5);
|
||||
assert_eq!(s.upper, 50);
|
||||
|
||||
// Check UpperSelected: upper increments
|
||||
s.state = SliderState::UpperSelected;
|
||||
s.increment_selected_fast();
|
||||
assert_eq!(s.lower, 5);
|
||||
assert_eq!(s.upper, 60);
|
||||
|
||||
// Check UpperSelected: upper can't go above max
|
||||
s.upper = s.max;
|
||||
s.increment_selected_fast();
|
||||
assert_eq!(s.lower, 5);
|
||||
assert_eq!(s.upper, s.max);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stateful_slider_decrement_selected_slow() {
|
||||
let mut s = initialize_slider(SliderState::LowerHover);
|
||||
s.min = 4;
|
||||
s.lower = 5;
|
||||
s.upper = 50;
|
||||
|
||||
// Check LowerHover: no decrement
|
||||
s.decrement_selected_slow();
|
||||
assert_eq!(s.lower, 5);
|
||||
|
||||
// Check LowerSelected: lower decrements
|
||||
s.state = SliderState::LowerSelected;
|
||||
s.decrement_selected_slow();
|
||||
assert_eq!(s.lower, 4);
|
||||
|
||||
// Check LowerSelected: lower can't go below min
|
||||
s.lower = s.min;
|
||||
s.decrement_selected_slow();
|
||||
assert_eq!(s.lower, s.min);
|
||||
|
||||
// Check LowerSelected: lower can't go below 0
|
||||
s.min = 0;
|
||||
s.lower = 0;
|
||||
s.decrement_selected_slow();
|
||||
assert_eq!(s.lower, 0);
|
||||
|
||||
// Check UpperHover: no decrement
|
||||
s.lower = 5;
|
||||
s.upper = 50;
|
||||
s.state = SliderState::UpperHover;
|
||||
s.decrement_selected_slow();
|
||||
assert_eq!(s.lower, 5);
|
||||
assert_eq!(s.upper, 50);
|
||||
|
||||
// Check UpperSelected: upper decrements
|
||||
s.state = SliderState::UpperSelected;
|
||||
s.decrement_selected_slow();
|
||||
assert_eq!(s.lower, 5);
|
||||
assert_eq!(s.upper, 49);
|
||||
|
||||
// Check UpperSelected: upper can't go below lower
|
||||
s.upper = s.lower;
|
||||
s.decrement_selected_slow();
|
||||
assert_eq!(s.lower, 5);
|
||||
assert_eq!(s.upper, s.lower);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stateful_slider_decrement_selected_fast() {
|
||||
let mut s = initialize_slider(SliderState::LowerHover);
|
||||
s.min = 20;
|
||||
s.lower = 35;
|
||||
s.upper = 50;
|
||||
|
||||
// Check LowerHover: no decrement
|
||||
s.decrement_selected_fast();
|
||||
assert_eq!(s.lower, 35);
|
||||
|
||||
// Check LowerSelected: lower decrements
|
||||
s.state = SliderState::LowerSelected;
|
||||
s.decrement_selected_fast();
|
||||
assert_eq!(s.lower, 25);
|
||||
|
||||
// Check LowerSelected: lower can't go below min
|
||||
s.lower = s.min;
|
||||
s.decrement_selected_fast();
|
||||
assert_eq!(s.lower, s.min);
|
||||
|
||||
// Check LowerSelected: lower can't go below 0
|
||||
s.min = 0;
|
||||
s.lower = 0;
|
||||
s.decrement_selected_fast();
|
||||
assert_eq!(s.lower, 0);
|
||||
|
||||
// Check UpperHover: no decrement
|
||||
s.lower = 5;
|
||||
s.upper = 50;
|
||||
s.state = SliderState::UpperHover;
|
||||
s.decrement_selected_fast();
|
||||
assert_eq!(s.lower, 5);
|
||||
assert_eq!(s.upper, 50);
|
||||
|
||||
// Check UpperSelected: upper decrements
|
||||
s.state = SliderState::UpperSelected;
|
||||
s.decrement_selected_fast();
|
||||
assert_eq!(s.lower, 5);
|
||||
assert_eq!(s.upper, 40);
|
||||
|
||||
// Check UpperSelected: upper can't go below lower
|
||||
s.upper = s.lower;
|
||||
s.decrement_selected_fast();
|
||||
assert_eq!(s.lower, 5);
|
||||
assert_eq!(s.upper, s.lower);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stateful_slider_select_deselect() {
|
||||
let mut s = initialize_slider(SliderState::LowerHover);
|
||||
assert_eq!(s.state, SliderState::LowerHover);
|
||||
s.select_deselect();
|
||||
assert_eq!(s.state, SliderState::LowerSelected);
|
||||
s.select_deselect();
|
||||
assert_eq!(s.state, SliderState::LowerHover);
|
||||
s.state = SliderState::UpperHover;
|
||||
s.select_deselect();
|
||||
assert_eq!(s.state, SliderState::UpperSelected);
|
||||
s.select_deselect();
|
||||
assert_eq!(s.state, SliderState::UpperHover);
|
||||
s.state = SliderState::None;
|
||||
s.select_deselect();
|
||||
assert_eq!(s.state, SliderState::None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stateful_slider_deselect() {
|
||||
let mut s = initialize_slider(SliderState::LowerHover);
|
||||
s.deselect();
|
||||
assert_eq!(s.state, SliderState::LowerHover);
|
||||
|
||||
s.state = SliderState::LowerSelected;
|
||||
s.deselect();
|
||||
assert_eq!(s.state, SliderState::LowerHover);
|
||||
|
||||
s.state = SliderState::UpperHover;
|
||||
s.deselect();
|
||||
assert_eq!(s.state, SliderState::UpperHover);
|
||||
|
||||
s.state = SliderState::UpperSelected;
|
||||
s.deselect();
|
||||
assert_eq!(s.state, SliderState::UpperHover);
|
||||
|
||||
s.state = SliderState::None;
|
||||
s.deselect();
|
||||
assert_eq!(s.state, SliderState::None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stateful_slider_switch_hover() {
|
||||
let mut s = initialize_slider(SliderState::LowerHover);
|
||||
s.switch_hover();
|
||||
assert_eq!(s.state, SliderState::UpperHover);
|
||||
|
||||
s.state = SliderState::LowerSelected;
|
||||
s.switch_hover();
|
||||
assert_eq!(s.state, SliderState::LowerSelected);
|
||||
|
||||
s.state = SliderState::UpperHover;
|
||||
s.switch_hover();
|
||||
assert_eq!(s.state, SliderState::LowerHover);
|
||||
|
||||
s.state = SliderState::UpperSelected;
|
||||
s.switch_hover();
|
||||
assert_eq!(s.state, SliderState::UpperSelected);
|
||||
|
||||
s.state = SliderState::None;
|
||||
s.switch_hover();
|
||||
assert_eq!(s.state, SliderState::None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stateful_slider_is_handle_selected() {
|
||||
let mut s = initialize_slider(SliderState::LowerHover);
|
||||
assert_eq!(s.is_handle_selected(), false);
|
||||
|
||||
s.state = SliderState::LowerSelected;
|
||||
assert_eq!(s.is_handle_selected(), true);
|
||||
|
||||
s.state = SliderState::UpperHover;
|
||||
assert_eq!(s.is_handle_selected(), false);
|
||||
|
||||
s.state = SliderState::UpperSelected;
|
||||
assert_eq!(s.is_handle_selected(), true);
|
||||
|
||||
s.state = SliderState::None;
|
||||
assert_eq!(s.is_handle_selected(), false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stateful_slider_serialize() {
|
||||
let mut s = initialize_slider(SliderState::LowerHover);
|
||||
let s_json = serde_json::to_string(&s).unwrap();
|
||||
assert_eq!(&s_json, "[0,150]");
|
||||
s.lower = 25;
|
||||
s.upper = 75;
|
||||
let s_json = serde_json::to_string(&s).unwrap();
|
||||
assert_eq!(&s_json, "[25,75]");
|
||||
}
|
342
training_mod_tui/tests/test_stateful_table.rs
Normal file
342
training_mod_tui/tests/test_stateful_table.rs
Normal file
|
@ -0,0 +1,342 @@
|
|||
use ratatui::widgets::{TableSelection, TableState};
|
||||
use training_mod_tui_2::StatefulTable;
|
||||
|
||||
fn initialize_table(row: usize, col: usize) -> StatefulTable<u8> {
|
||||
let mut s = StatefulTable::with_items(2, 3, vec![0, 1, 2, 3, 4]);
|
||||
s.select(row, col);
|
||||
s
|
||||
}
|
||||
|
||||
fn tablestate_with(row: usize, col: usize) -> TableState {
|
||||
TableState::default().with_selected(Some(TableSelection::Cell { row, col }))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stateful_table_next_col_full() {
|
||||
let mut t = initialize_table(0, 0);
|
||||
assert_eq!(t.get_selected(), Some(&mut 0));
|
||||
t.next_col();
|
||||
assert_eq!(t.get_selected(), Some(&mut 1));
|
||||
t.next_col();
|
||||
assert_eq!(t.get_selected(), Some(&mut 2));
|
||||
t.next_col();
|
||||
assert_eq!(t.get_selected(), Some(&mut 0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stateful_table_next_col_checked_full() {
|
||||
let mut t = initialize_table(0, 0);
|
||||
assert_eq!(t.get_selected(), Some(&mut 0));
|
||||
t.next_col_checked();
|
||||
assert_eq!(t.get_selected(), Some(&mut 1));
|
||||
t.next_col_checked();
|
||||
assert_eq!(t.get_selected(), Some(&mut 2));
|
||||
t.next_col_checked();
|
||||
assert_eq!(t.get_selected(), Some(&mut 0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stateful_table_prev_col_full() {
|
||||
let mut t = initialize_table(0, 0);
|
||||
assert_eq!(t.get_selected(), Some(&mut 0));
|
||||
t.prev_col();
|
||||
assert_eq!(t.get_selected(), Some(&mut 2));
|
||||
t.prev_col();
|
||||
assert_eq!(t.get_selected(), Some(&mut 1));
|
||||
t.prev_col();
|
||||
assert_eq!(t.get_selected(), Some(&mut 0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stateful_table_prev_col_checked_full() {
|
||||
let mut t = initialize_table(0, 0);
|
||||
assert_eq!(t.get_selected(), Some(&mut 0));
|
||||
t.prev_col_checked();
|
||||
assert_eq!(t.get_selected(), Some(&mut 2));
|
||||
t.prev_col_checked();
|
||||
assert_eq!(t.get_selected(), Some(&mut 1));
|
||||
t.prev_col_checked();
|
||||
assert_eq!(t.get_selected(), Some(&mut 0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stateful_table_next_col_short() {
|
||||
let mut t = initialize_table(1, 0);
|
||||
assert_eq!(t.get_selected(), Some(&mut 3));
|
||||
t.next_col();
|
||||
assert_eq!(t.get_selected(), Some(&mut 4));
|
||||
t.next_col();
|
||||
assert_eq!(t.get_selected(), None);
|
||||
t.next_col();
|
||||
assert_eq!(t.get_selected(), Some(&mut 3));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stateful_table_next_col_checked_short() {
|
||||
let mut t = initialize_table(1, 0);
|
||||
assert_eq!(t.get_selected(), Some(&mut 3));
|
||||
t.next_col_checked();
|
||||
assert_eq!(t.get_selected(), Some(&mut 4));
|
||||
t.next_col_checked();
|
||||
assert_eq!(t.get_selected(), Some(&mut 3));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stateful_table_prev_col_short() {
|
||||
let mut t = initialize_table(1, 0);
|
||||
assert_eq!(t.get_selected(), Some(&mut 3));
|
||||
t.prev_col();
|
||||
assert_eq!(t.get_selected(), None);
|
||||
t.prev_col();
|
||||
assert_eq!(t.get_selected(), Some(&mut 4));
|
||||
t.prev_col();
|
||||
assert_eq!(t.get_selected(), Some(&mut 3));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stateful_table_carriage_return_none() {
|
||||
let mut t = initialize_table(1, 2);
|
||||
t.carriage_return();
|
||||
assert_eq!(t.state, tablestate_with(1, 1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stateful_table_carriage_return_some() {
|
||||
let mut t = initialize_table(1, 1);
|
||||
t.carriage_return();
|
||||
assert_eq!(t.state, tablestate_with(1, 1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stateful_table_table_with_items() {
|
||||
let items: Vec<u8> = vec![0, 1, 2, 3, 4];
|
||||
let t: StatefulTable<u8> = StatefulTable::with_items(2, 3, items);
|
||||
let u = initialize_table(0, 0);
|
||||
assert_eq!(t, u);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stateful_table_get_selected() {
|
||||
let mut t = initialize_table(1, 1);
|
||||
assert_eq!(t.get_selected(), Some(&mut 4));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stateful_table_get() {
|
||||
let t = initialize_table(1, 1);
|
||||
assert_eq!(t.get(0, 0), Some(&0));
|
||||
assert_eq!(t.get(0, 1), Some(&1));
|
||||
assert_eq!(t.get(0, 2), Some(&2));
|
||||
assert_eq!(t.get(1, 0), Some(&3));
|
||||
assert_eq!(t.get(1, 1), Some(&4));
|
||||
assert_eq!(t.get(1, 2), None);
|
||||
assert_eq!(t.get(10, 0), None);
|
||||
assert_eq!(t.get(0, 10), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stateful_table_get_by_idx() {
|
||||
let t = initialize_table(1, 1);
|
||||
assert_eq!(t.get_by_idx(0), Some(&0));
|
||||
assert_eq!(t.get_by_idx(1), Some(&1));
|
||||
assert_eq!(t.get_by_idx(2), Some(&2));
|
||||
assert_eq!(t.get_by_idx(3), Some(&3));
|
||||
assert_eq!(t.get_by_idx(4), Some(&4));
|
||||
assert_eq!(t.get_by_idx(5), None);
|
||||
assert_eq!(t.get_by_idx(50), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stateful_table_len() {
|
||||
let t = initialize_table(1, 1);
|
||||
assert_eq!(t.len(), 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stateful_table_full_len() {
|
||||
let t = initialize_table(0, 0);
|
||||
assert_eq!(t.full_len(), 6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stateful_table_serialize() {
|
||||
let t = initialize_table(1, 1);
|
||||
let t_ser = serde_json::to_string(&t).unwrap();
|
||||
assert_eq!(&t_ser, "[0,1,2,3,4]");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stateful_table_new() {
|
||||
let t: StatefulTable<u8> = StatefulTable::new(2, 3);
|
||||
let u: StatefulTable<u8> = StatefulTable::with_items(2, 3, vec![]);
|
||||
let v: StatefulTable<u8> = StatefulTable {
|
||||
state: tablestate_with(0, 0),
|
||||
items: vec![vec![None; 3]; 2],
|
||||
rows: 2,
|
||||
cols: 3,
|
||||
};
|
||||
assert_eq!(t, u);
|
||||
assert_eq!(t, v);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stateful_table_with_items() {
|
||||
let t: StatefulTable<u8> = StatefulTable::with_items(2, 3, vec![1, 2]);
|
||||
let u: StatefulTable<u8> = StatefulTable {
|
||||
state: tablestate_with(0, 0),
|
||||
items: vec![vec![Some(1), Some(2), None], vec![None; 3]],
|
||||
rows: 2,
|
||||
cols: 3,
|
||||
};
|
||||
assert_eq!(t, u);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stateful_table_select() {
|
||||
let mut t = initialize_table(0, 0);
|
||||
assert_eq!(t.get_selected(), Some(&mut 0));
|
||||
t.select(0, 1);
|
||||
assert_eq!(t.get_selected(), Some(&mut 1));
|
||||
t.select(0, 2);
|
||||
assert_eq!(t.get_selected(), Some(&mut 2));
|
||||
t.select(1, 0);
|
||||
assert_eq!(t.get_selected(), Some(&mut 3));
|
||||
t.select(1, 1);
|
||||
assert_eq!(t.get_selected(), Some(&mut 4));
|
||||
t.select(1, 2);
|
||||
assert_eq!(t.get_selected(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stateful_table_get_mut() {
|
||||
let mut t = initialize_table(1, 1);
|
||||
assert_eq!(t.get_mut(0, 0), Some(&mut 0));
|
||||
assert_eq!(t.get_mut(0, 1), Some(&mut 1));
|
||||
assert_eq!(t.get_mut(0, 2), Some(&mut 2));
|
||||
assert_eq!(t.get_mut(1, 0), Some(&mut 3));
|
||||
assert_eq!(t.get_mut(1, 1), Some(&mut 4));
|
||||
assert_eq!(t.get_mut(1, 2), None);
|
||||
assert_eq!(t.get_mut(10, 0), None);
|
||||
assert_eq!(t.get_mut(0, 10), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stateful_table_get_by_idx_mut() {
|
||||
let mut t = initialize_table(1, 1);
|
||||
assert_eq!(t.get_by_idx_mut(0), Some(&mut 0));
|
||||
assert_eq!(t.get_by_idx_mut(1), Some(&mut 1));
|
||||
assert_eq!(t.get_by_idx_mut(2), Some(&mut 2));
|
||||
assert_eq!(t.get_by_idx_mut(3), Some(&mut 3));
|
||||
assert_eq!(t.get_by_idx_mut(4), Some(&mut 4));
|
||||
assert_eq!(t.get_by_idx_mut(5), None);
|
||||
assert_eq!(t.get_by_idx_mut(50), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stateful_table_next_row_full() {
|
||||
let mut t = initialize_table(0, 0);
|
||||
assert_eq!(t.get_selected(), Some(&mut 0));
|
||||
t.next_row();
|
||||
assert_eq!(t.get_selected(), Some(&mut 3));
|
||||
t.next_row();
|
||||
assert_eq!(t.get_selected(), Some(&mut 0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stateful_table_next_row_short() {
|
||||
let mut t = initialize_table(0, 2);
|
||||
assert_eq!(t.get_selected(), Some(&mut 2));
|
||||
t.next_row();
|
||||
assert_eq!(t.get_selected(), None);
|
||||
t.next_row();
|
||||
assert_eq!(t.get_selected(), Some(&mut 2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stateful_table_next_row_checked_full() {
|
||||
let mut t = initialize_table(0, 0);
|
||||
assert_eq!(t.get_selected(), Some(&mut 0));
|
||||
t.next_row_checked();
|
||||
assert_eq!(t.get_selected(), Some(&mut 3));
|
||||
t.next_row_checked();
|
||||
assert_eq!(t.get_selected(), Some(&mut 0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stateful_table_next_row_checked_short() {
|
||||
let mut t = initialize_table(0, 2);
|
||||
assert_eq!(t.get_selected(), Some(&mut 2));
|
||||
t.next_row_checked();
|
||||
assert_eq!(t.get_selected(), Some(&mut 2));
|
||||
t.next_row_checked();
|
||||
assert_eq!(t.get_selected(), Some(&mut 2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stateful_table_prev_row_full() {
|
||||
let mut t = initialize_table(0, 0);
|
||||
assert_eq!(t.get_selected(), Some(&mut 0));
|
||||
t.prev_row();
|
||||
assert_eq!(t.get_selected(), Some(&mut 3));
|
||||
t.prev_row();
|
||||
assert_eq!(t.get_selected(), Some(&mut 0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stateful_table_prev_row_short() {
|
||||
let mut t = initialize_table(0, 2);
|
||||
assert_eq!(t.get_selected(), Some(&mut 2));
|
||||
t.prev_row();
|
||||
assert_eq!(t.get_selected(), None);
|
||||
t.prev_row();
|
||||
assert_eq!(t.get_selected(), Some(&mut 2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stateful_table_prev_row_checked_full() {
|
||||
let mut t = initialize_table(0, 0);
|
||||
assert_eq!(t.get_selected(), Some(&mut 0));
|
||||
t.prev_row_checked();
|
||||
assert_eq!(t.get_selected(), Some(&mut 3));
|
||||
t.prev_row_checked();
|
||||
assert_eq!(t.get_selected(), Some(&mut 0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stateful_table_prev_row_checked_short() {
|
||||
let mut t = initialize_table(0, 2);
|
||||
assert_eq!(t.get_selected(), Some(&mut 2));
|
||||
t.prev_row_checked();
|
||||
assert_eq!(t.get_selected(), Some(&mut 2));
|
||||
t.prev_row_checked();
|
||||
assert_eq!(t.get_selected(), Some(&mut 2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stateful_table_iter() {
|
||||
let t = initialize_table(0, 0);
|
||||
let mut t_iter = t.iter();
|
||||
assert_eq!(t_iter.next(), Some(&0));
|
||||
assert_eq!(t_iter.next(), Some(&1));
|
||||
assert_eq!(t_iter.next(), Some(&2));
|
||||
assert_eq!(t_iter.next(), Some(&3));
|
||||
assert_eq!(t_iter.next(), Some(&4));
|
||||
assert_eq!(t_iter.next(), None);
|
||||
assert_eq!(t_iter.next(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stateful_table_iter_mut() {
|
||||
let mut t = initialize_table(0, 0);
|
||||
for item in t.iter_mut().filter(|item| item.is_some()) {
|
||||
*item = Some(item.unwrap() + 10);
|
||||
}
|
||||
let mut t_iter = t.iter();
|
||||
assert_eq!(t_iter.next(), Some(&10));
|
||||
assert_eq!(t_iter.next(), Some(&11));
|
||||
assert_eq!(t_iter.next(), Some(&12));
|
||||
assert_eq!(t_iter.next(), Some(&13));
|
||||
assert_eq!(t_iter.next(), Some(&14));
|
||||
assert_eq!(t_iter.next(), None);
|
||||
assert_eq!(t_iter.next(), None);
|
||||
}
|
502
training_mod_tui/tests/test_submenu.rs
Normal file
502
training_mod_tui/tests/test_submenu.rs
Normal file
|
@ -0,0 +1,502 @@
|
|||
use ratatui::widgets::{TableSelection, TableState};
|
||||
use training_mod_tui_2::*;
|
||||
|
||||
fn make_toggle<'a>(v: u8) -> Toggle<'a> {
|
||||
Toggle {
|
||||
title: "Title",
|
||||
value: v,
|
||||
max: 4,
|
||||
}
|
||||
}
|
||||
|
||||
fn make_toggle_table_multiple<'a>(
|
||||
rows: usize,
|
||||
cols: usize,
|
||||
num: usize,
|
||||
) -> StatefulTable<Toggle<'a>> {
|
||||
// [ (0) 1 2 ]
|
||||
// [ 3 ]
|
||||
let v: Vec<Toggle> = (0..num).map(|v| make_toggle(v as u8)).collect();
|
||||
StatefulTable::with_items(rows, cols, v)
|
||||
}
|
||||
|
||||
fn make_toggle_table_single<'a>(rows: usize, cols: usize, num: usize) -> StatefulTable<Toggle<'a>> {
|
||||
// [ (1) 0 0 ]
|
||||
// [ 0 ]
|
||||
let v: Vec<Toggle> = (0..num).map(|_| make_toggle(0)).collect();
|
||||
let mut t = StatefulTable::with_items(rows, cols, v);
|
||||
t.items[0][0] = Some(make_toggle(1));
|
||||
t
|
||||
}
|
||||
|
||||
fn initialize_submenu<'a>(submenu_type: SubMenuType) -> SubMenu<'a> {
|
||||
match submenu_type {
|
||||
SubMenuType::ToggleSingle => SubMenu {
|
||||
title: "Single Option Menu",
|
||||
id: "single_option",
|
||||
help_text: "A Single Option",
|
||||
submenu_type: submenu_type,
|
||||
toggles: make_toggle_table_single(2, 3, 4),
|
||||
slider: None,
|
||||
},
|
||||
SubMenuType::ToggleMultiple => SubMenu {
|
||||
title: "Multi Option Menu",
|
||||
id: "multi_option",
|
||||
help_text: "Multiple Options",
|
||||
submenu_type: submenu_type,
|
||||
toggles: make_toggle_table_multiple(2, 3, 4),
|
||||
slider: None,
|
||||
},
|
||||
SubMenuType::Slider => SubMenu {
|
||||
title: "Slider Menu",
|
||||
id: "slider",
|
||||
help_text: "A Double-ended Slider",
|
||||
submenu_type: submenu_type,
|
||||
toggles: make_toggle_table_multiple(0, 0, 0),
|
||||
slider: Some(StatefulSlider::new()),
|
||||
},
|
||||
SubMenuType::None => {
|
||||
panic!()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn submenu_serialize() {
|
||||
let submenu = initialize_submenu(SubMenuType::ToggleSingle);
|
||||
let json = serde_json::to_string(&submenu).unwrap();
|
||||
assert_eq!(&json, "[1,0,0,0]");
|
||||
|
||||
let submenu = initialize_submenu(SubMenuType::ToggleMultiple);
|
||||
let json = serde_json::to_string(&submenu).unwrap();
|
||||
assert_eq!(&json, "[0,1,2,3]");
|
||||
|
||||
let submenu = initialize_submenu(SubMenuType::Slider);
|
||||
let json = serde_json::to_string(&submenu).unwrap();
|
||||
assert_eq!(&json, "[0,150]");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn submenu_selected_toggle() {
|
||||
let mut submenu = initialize_submenu(SubMenuType::ToggleSingle);
|
||||
let mut t = make_toggle(1);
|
||||
assert_eq!(submenu.selected_toggle(), &mut t);
|
||||
t = make_toggle(0);
|
||||
submenu.toggles.next_col();
|
||||
assert_eq!(submenu.selected_toggle(), &mut t);
|
||||
|
||||
let mut submenu = initialize_submenu(SubMenuType::ToggleMultiple);
|
||||
let mut t = make_toggle(0);
|
||||
assert_eq!(submenu.selected_toggle(), &mut t);
|
||||
t = make_toggle(1);
|
||||
submenu.toggles.next_col();
|
||||
assert_eq!(submenu.selected_toggle(), &mut t);
|
||||
t = make_toggle(2);
|
||||
submenu.toggles.next_col();
|
||||
assert_eq!(submenu.selected_toggle(), &mut t);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn submenu_update_from_vec() {
|
||||
let mut submenu = initialize_submenu(SubMenuType::ToggleSingle);
|
||||
assert_eq!(submenu.toggles.items[0][0], Some(make_toggle(1)));
|
||||
assert_eq!(submenu.toggles.items[0][1], Some(make_toggle(0)));
|
||||
assert_eq!(submenu.toggles.items[0][2], Some(make_toggle(0)));
|
||||
assert_eq!(submenu.toggles.items[1][0], Some(make_toggle(0)));
|
||||
assert_eq!(submenu.toggles.items[1][1], None);
|
||||
assert_eq!(submenu.toggles.items[1][2], None);
|
||||
submenu.update_from_vec(vec![0, 0, 1, 0]);
|
||||
assert_eq!(submenu.toggles.items[0][0], Some(make_toggle(0)));
|
||||
assert_eq!(submenu.toggles.items[0][1], Some(make_toggle(0)));
|
||||
assert_eq!(submenu.toggles.items[0][2], Some(make_toggle(1)));
|
||||
assert_eq!(submenu.toggles.items[1][0], Some(make_toggle(0)));
|
||||
assert_eq!(submenu.toggles.items[1][1], None);
|
||||
assert_eq!(submenu.toggles.items[1][2], None);
|
||||
|
||||
let mut submenu = initialize_submenu(SubMenuType::ToggleMultiple);
|
||||
assert_eq!(submenu.toggles.items[0][0], Some(make_toggle(0)));
|
||||
assert_eq!(submenu.toggles.items[0][1], Some(make_toggle(1)));
|
||||
assert_eq!(submenu.toggles.items[0][2], Some(make_toggle(2)));
|
||||
assert_eq!(submenu.toggles.items[1][0], Some(make_toggle(3)));
|
||||
assert_eq!(submenu.toggles.items[1][1], None);
|
||||
assert_eq!(submenu.toggles.items[1][2], None);
|
||||
submenu.update_from_vec(vec![1, 1, 0, 4]);
|
||||
assert_eq!(submenu.toggles.items[0][0], Some(make_toggle(1)));
|
||||
assert_eq!(submenu.toggles.items[0][1], Some(make_toggle(1)));
|
||||
assert_eq!(submenu.toggles.items[0][2], Some(make_toggle(0)));
|
||||
assert_eq!(submenu.toggles.items[1][0], Some(make_toggle(4)));
|
||||
assert_eq!(submenu.toggles.items[1][1], None);
|
||||
assert_eq!(submenu.toggles.items[1][2], None);
|
||||
|
||||
let mut submenu = initialize_submenu(SubMenuType::Slider);
|
||||
let mut slider = StatefulSlider::new();
|
||||
assert_eq!(submenu.slider, Some(slider));
|
||||
slider.lower = 5;
|
||||
submenu.update_from_vec(vec![5, 150]);
|
||||
assert_eq!(submenu.slider, Some(slider));
|
||||
slider.upper = 75;
|
||||
submenu.update_from_vec(vec![5, 75]);
|
||||
assert_eq!(submenu.slider, Some(slider));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn submenu_single_on_a() {
|
||||
let mut submenu = initialize_submenu(SubMenuType::ToggleSingle);
|
||||
submenu.toggles.select(1, 0);
|
||||
submenu.on_a();
|
||||
assert_eq!(submenu.toggles.items[0][0], Some(make_toggle(0)));
|
||||
assert_eq!(submenu.toggles.items[0][1], Some(make_toggle(0)));
|
||||
assert_eq!(submenu.toggles.items[0][2], Some(make_toggle(0)));
|
||||
assert_eq!(submenu.toggles.items[1][0], Some(make_toggle(1)));
|
||||
assert_eq!(submenu.toggles.items[1][1], None);
|
||||
assert_eq!(submenu.toggles.items[1][2], None);
|
||||
submenu.on_a();
|
||||
assert_eq!(submenu.toggles.items[0][0], Some(make_toggle(0)));
|
||||
assert_eq!(submenu.toggles.items[0][1], Some(make_toggle(0)));
|
||||
assert_eq!(submenu.toggles.items[0][2], Some(make_toggle(0)));
|
||||
assert_eq!(submenu.toggles.items[1][0], Some(make_toggle(1)));
|
||||
assert_eq!(submenu.toggles.items[1][1], None);
|
||||
assert_eq!(submenu.toggles.items[1][2], None);
|
||||
submenu.toggles.select(0, 0);
|
||||
submenu.on_a();
|
||||
assert_eq!(submenu.toggles.items[0][0], Some(make_toggle(1)));
|
||||
assert_eq!(submenu.toggles.items[0][1], Some(make_toggle(0)));
|
||||
assert_eq!(submenu.toggles.items[0][2], Some(make_toggle(0)));
|
||||
assert_eq!(submenu.toggles.items[1][0], Some(make_toggle(0)));
|
||||
assert_eq!(submenu.toggles.items[1][1], None);
|
||||
assert_eq!(submenu.toggles.items[1][2], None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn submenu_multiple_on_a() {
|
||||
let mut submenu = initialize_submenu(SubMenuType::ToggleMultiple);
|
||||
submenu.toggles.select(1, 0);
|
||||
submenu.on_a();
|
||||
assert_eq!(submenu.toggles.items[0][0], Some(make_toggle(0)));
|
||||
assert_eq!(submenu.toggles.items[0][1], Some(make_toggle(1)));
|
||||
assert_eq!(submenu.toggles.items[0][2], Some(make_toggle(2)));
|
||||
assert_eq!(submenu.toggles.items[1][0], Some(make_toggle(4)));
|
||||
assert_eq!(submenu.toggles.items[1][1], None);
|
||||
assert_eq!(submenu.toggles.items[1][2], None);
|
||||
submenu.on_a();
|
||||
assert_eq!(submenu.toggles.items[0][0], Some(make_toggle(0)));
|
||||
assert_eq!(submenu.toggles.items[0][1], Some(make_toggle(1)));
|
||||
assert_eq!(submenu.toggles.items[0][2], Some(make_toggle(2)));
|
||||
assert_eq!(submenu.toggles.items[1][0], Some(make_toggle(0)));
|
||||
assert_eq!(submenu.toggles.items[1][1], None);
|
||||
assert_eq!(submenu.toggles.items[1][2], None);
|
||||
submenu.toggles.select(0, 0);
|
||||
submenu.on_a();
|
||||
assert_eq!(submenu.toggles.items[0][0], Some(make_toggle(1)));
|
||||
assert_eq!(submenu.toggles.items[0][1], Some(make_toggle(1)));
|
||||
assert_eq!(submenu.toggles.items[0][2], Some(make_toggle(2)));
|
||||
assert_eq!(submenu.toggles.items[1][0], Some(make_toggle(0)));
|
||||
assert_eq!(submenu.toggles.items[1][1], None);
|
||||
assert_eq!(submenu.toggles.items[1][2], None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn submenu_slider_on_a() {
|
||||
let mut submenu = initialize_submenu(SubMenuType::Slider);
|
||||
assert_eq!(submenu.slider.unwrap().state, SliderState::LowerHover);
|
||||
submenu.on_a();
|
||||
assert_eq!(submenu.slider.unwrap().state, SliderState::LowerSelected);
|
||||
submenu.on_a();
|
||||
assert_eq!(submenu.slider.unwrap().state, SliderState::LowerHover);
|
||||
submenu.slider = Some(StatefulSlider {
|
||||
state: SliderState::UpperHover,
|
||||
..submenu.slider.unwrap()
|
||||
});
|
||||
assert_eq!(submenu.slider.unwrap().state, SliderState::UpperHover);
|
||||
submenu.on_a();
|
||||
assert_eq!(submenu.slider.unwrap().state, SliderState::UpperSelected);
|
||||
submenu.on_a();
|
||||
assert_eq!(submenu.slider.unwrap().state, SliderState::UpperHover);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn submenu_slider_on_b_selected() {
|
||||
let mut submenu = initialize_submenu(SubMenuType::Slider);
|
||||
submenu.slider = Some(StatefulSlider {
|
||||
state: SliderState::LowerSelected,
|
||||
..submenu.slider.unwrap()
|
||||
});
|
||||
submenu.on_b();
|
||||
assert_eq!(submenu.slider.unwrap().state, SliderState::LowerHover);
|
||||
submenu.on_b();
|
||||
assert_eq!(submenu.slider.unwrap().state, SliderState::LowerHover);
|
||||
submenu.slider = Some(StatefulSlider {
|
||||
state: SliderState::UpperSelected,
|
||||
..submenu.slider.unwrap()
|
||||
});
|
||||
submenu.on_b();
|
||||
assert_eq!(submenu.slider.unwrap().state, SliderState::UpperHover);
|
||||
submenu.on_b();
|
||||
assert_eq!(submenu.slider.unwrap().state, SliderState::UpperHover);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn submenu_single_on_up() {
|
||||
let mut submenu = initialize_submenu(SubMenuType::ToggleSingle);
|
||||
let mut state = TableState::default();
|
||||
state.select(Some(TableSelection::Cell { row: 0, col: 0 }));
|
||||
assert_eq!(submenu.toggles.state, state);
|
||||
submenu.on_up();
|
||||
state.select(Some(TableSelection::Cell { row: 1, col: 0 }));
|
||||
assert_eq!(submenu.toggles.state, state);
|
||||
submenu.on_up();
|
||||
state.select(Some(TableSelection::Cell { row: 0, col: 0 }));
|
||||
assert_eq!(submenu.toggles.state, state);
|
||||
|
||||
submenu.toggles.select(0, 2);
|
||||
state.select(Some(TableSelection::Cell { row: 0, col: 2 }));
|
||||
assert_eq!(submenu.toggles.state, state);
|
||||
submenu.on_up();
|
||||
assert_eq!(submenu.toggles.state, state);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn submenu_multiple_on_up() {
|
||||
let mut submenu = initialize_submenu(SubMenuType::ToggleMultiple);
|
||||
let mut state = TableState::default();
|
||||
state.select(Some(TableSelection::Cell { row: 0, col: 0 }));
|
||||
assert_eq!(submenu.toggles.state, state);
|
||||
submenu.on_up();
|
||||
state.select(Some(TableSelection::Cell { row: 1, col: 0 }));
|
||||
assert_eq!(submenu.toggles.state, state);
|
||||
submenu.on_up();
|
||||
state.select(Some(TableSelection::Cell { row: 0, col: 0 }));
|
||||
assert_eq!(submenu.toggles.state, state);
|
||||
|
||||
submenu.toggles.select(0, 2);
|
||||
state.select(Some(TableSelection::Cell { row: 0, col: 2 }));
|
||||
assert_eq!(submenu.toggles.state, state);
|
||||
submenu.on_up();
|
||||
assert_eq!(submenu.toggles.state, state);
|
||||
}
|
||||
#[test]
|
||||
fn submenu_single_on_down() {
|
||||
let mut submenu = initialize_submenu(SubMenuType::ToggleSingle);
|
||||
let mut state = TableState::default();
|
||||
state.select(Some(TableSelection::Cell { row: 0, col: 0 }));
|
||||
assert_eq!(submenu.toggles.state, state);
|
||||
submenu.on_down();
|
||||
state.select(Some(TableSelection::Cell { row: 1, col: 0 }));
|
||||
assert_eq!(submenu.toggles.state, state);
|
||||
submenu.on_down();
|
||||
state.select(Some(TableSelection::Cell { row: 0, col: 0 }));
|
||||
assert_eq!(submenu.toggles.state, state);
|
||||
|
||||
submenu.toggles.select(0, 2);
|
||||
state.select(Some(TableSelection::Cell { row: 0, col: 2 }));
|
||||
assert_eq!(submenu.toggles.state, state);
|
||||
submenu.on_down();
|
||||
assert_eq!(submenu.toggles.state, state);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn submenu_multiple_on_down() {
|
||||
let mut submenu = initialize_submenu(SubMenuType::ToggleMultiple);
|
||||
let mut state = TableState::default();
|
||||
state.select(Some(TableSelection::Cell { row: 0, col: 0 }));
|
||||
assert_eq!(submenu.toggles.state, state);
|
||||
submenu.on_down();
|
||||
state.select(Some(TableSelection::Cell { row: 1, col: 0 }));
|
||||
assert_eq!(submenu.toggles.state, state);
|
||||
submenu.on_down();
|
||||
state.select(Some(TableSelection::Cell { row: 0, col: 0 }));
|
||||
assert_eq!(submenu.toggles.state, state);
|
||||
|
||||
submenu.toggles.select(0, 2);
|
||||
state.select(Some(TableSelection::Cell { row: 0, col: 2 }));
|
||||
assert_eq!(submenu.toggles.state, state);
|
||||
submenu.on_down();
|
||||
assert_eq!(submenu.toggles.state, state);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn submenu_single_on_left() {
|
||||
let mut submenu = initialize_submenu(SubMenuType::ToggleSingle);
|
||||
let mut state = TableState::default();
|
||||
state.select(Some(TableSelection::Cell { row: 0, col: 0 }));
|
||||
assert_eq!(submenu.toggles.state, state);
|
||||
assert_eq!(submenu.toggles.get_selected(), Some(&mut make_toggle(1)));
|
||||
submenu.on_left();
|
||||
state.select(Some(TableSelection::Cell { row: 0, col: 2 }));
|
||||
assert_eq!(submenu.toggles.state, state);
|
||||
assert_eq!(submenu.toggles.get_selected(), Some(&mut make_toggle(0)));
|
||||
submenu.on_left();
|
||||
state.select(Some(TableSelection::Cell { row: 0, col: 1 }));
|
||||
assert_eq!(submenu.toggles.state, state);
|
||||
assert_eq!(submenu.toggles.get_selected(), Some(&mut make_toggle(0)));
|
||||
|
||||
submenu.toggles.select(1, 0);
|
||||
submenu.on_left();
|
||||
state.select(Some(TableSelection::Cell { row: 1, col: 0 }));
|
||||
assert_eq!(submenu.toggles.state, state);
|
||||
assert_eq!(submenu.toggles.get_selected(), Some(&mut make_toggle(0)));
|
||||
submenu.on_left();
|
||||
assert_eq!(submenu.toggles.state, state);
|
||||
assert_eq!(submenu.toggles.get_selected(), Some(&mut make_toggle(0)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn submenu_multiple_on_left() {
|
||||
let mut submenu = initialize_submenu(SubMenuType::ToggleMultiple);
|
||||
let mut state = TableState::default();
|
||||
state.select(Some(TableSelection::Cell { row: 0, col: 0 }));
|
||||
assert_eq!(submenu.toggles.state, state);
|
||||
assert_eq!(submenu.toggles.get_selected(), Some(&mut make_toggle(0)));
|
||||
submenu.on_left();
|
||||
state.select(Some(TableSelection::Cell { row: 0, col: 2 }));
|
||||
assert_eq!(submenu.toggles.state, state);
|
||||
assert_eq!(submenu.toggles.get_selected(), Some(&mut make_toggle(2)));
|
||||
submenu.on_left();
|
||||
state.select(Some(TableSelection::Cell { row: 0, col: 1 }));
|
||||
assert_eq!(submenu.toggles.state, state);
|
||||
assert_eq!(submenu.toggles.get_selected(), Some(&mut make_toggle(1)));
|
||||
|
||||
submenu.toggles.select(1, 0);
|
||||
submenu.on_left();
|
||||
state.select(Some(TableSelection::Cell { row: 1, col: 0 }));
|
||||
assert_eq!(submenu.toggles.state, state);
|
||||
assert_eq!(submenu.toggles.get_selected(), Some(&mut make_toggle(3)));
|
||||
submenu.on_left();
|
||||
assert_eq!(submenu.toggles.state, state);
|
||||
assert_eq!(submenu.toggles.get_selected(), Some(&mut make_toggle(3)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn submenu_slider_on_left() {
|
||||
let mut submenu = initialize_submenu(SubMenuType::Slider);
|
||||
let mut state = SliderState::LowerHover;
|
||||
assert_eq!(submenu.slider.unwrap().state, state);
|
||||
state = SliderState::UpperHover;
|
||||
submenu.on_left();
|
||||
assert_eq!(submenu.slider.unwrap().state, state);
|
||||
|
||||
submenu.slider = Some(StatefulSlider {
|
||||
state: SliderState::LowerSelected,
|
||||
lower: 1,
|
||||
..submenu.slider.unwrap()
|
||||
});
|
||||
state = SliderState::LowerSelected;
|
||||
submenu.on_left();
|
||||
assert_eq!(submenu.slider.unwrap().state, state);
|
||||
assert_eq!(submenu.slider.unwrap().lower, 0);
|
||||
assert_eq!(submenu.slider.unwrap().upper, 150);
|
||||
submenu.on_left();
|
||||
assert_eq!(submenu.slider.unwrap().state, state);
|
||||
assert_eq!(submenu.slider.unwrap().lower, 0);
|
||||
assert_eq!(submenu.slider.unwrap().upper, 150);
|
||||
|
||||
submenu.slider = Some(StatefulSlider {
|
||||
state: SliderState::UpperSelected,
|
||||
lower: 99,
|
||||
upper: 100,
|
||||
..submenu.slider.unwrap()
|
||||
});
|
||||
state = SliderState::UpperSelected;
|
||||
submenu.on_left();
|
||||
assert_eq!(submenu.slider.unwrap().state, state);
|
||||
assert_eq!(submenu.slider.unwrap().lower, 99);
|
||||
assert_eq!(submenu.slider.unwrap().upper, 99);
|
||||
submenu.on_left();
|
||||
assert_eq!(submenu.slider.unwrap().state, state);
|
||||
assert_eq!(submenu.slider.unwrap().lower, 99);
|
||||
assert_eq!(submenu.slider.unwrap().upper, 99);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn submenu_single_on_right() {
|
||||
let mut submenu = initialize_submenu(SubMenuType::ToggleSingle);
|
||||
let mut state = TableState::default();
|
||||
state.select(Some(TableSelection::Cell { row: 0, col: 0 }));
|
||||
assert_eq!(submenu.toggles.state, state);
|
||||
assert_eq!(submenu.toggles.get_selected(), Some(&mut make_toggle(1)));
|
||||
submenu.on_right();
|
||||
state.select(Some(TableSelection::Cell { row: 0, col: 1 }));
|
||||
assert_eq!(submenu.toggles.state, state);
|
||||
assert_eq!(submenu.toggles.get_selected(), Some(&mut make_toggle(0)));
|
||||
submenu.on_right();
|
||||
state.select(Some(TableSelection::Cell { row: 0, col: 2 }));
|
||||
assert_eq!(submenu.toggles.state, state);
|
||||
assert_eq!(submenu.toggles.get_selected(), Some(&mut make_toggle(0)));
|
||||
|
||||
submenu.toggles.select(1, 0);
|
||||
submenu.on_right();
|
||||
state.select(Some(TableSelection::Cell { row: 1, col: 0 }));
|
||||
assert_eq!(submenu.toggles.state, state);
|
||||
assert_eq!(submenu.toggles.get_selected(), Some(&mut make_toggle(0)));
|
||||
submenu.on_right();
|
||||
assert_eq!(submenu.toggles.state, state);
|
||||
assert_eq!(submenu.toggles.get_selected(), Some(&mut make_toggle(0)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn submenu_multiple_on_right() {
|
||||
let mut submenu = initialize_submenu(SubMenuType::ToggleMultiple);
|
||||
let mut state = TableState::default();
|
||||
state.select(Some(TableSelection::Cell { row: 0, col: 0 }));
|
||||
assert_eq!(submenu.toggles.state, state);
|
||||
assert_eq!(submenu.toggles.get_selected(), Some(&mut make_toggle(0)));
|
||||
submenu.on_right();
|
||||
state.select(Some(TableSelection::Cell { row: 0, col: 1 }));
|
||||
assert_eq!(submenu.toggles.state, state);
|
||||
assert_eq!(submenu.toggles.get_selected(), Some(&mut make_toggle(1)));
|
||||
submenu.on_right();
|
||||
state.select(Some(TableSelection::Cell { row: 0, col: 2 }));
|
||||
assert_eq!(submenu.toggles.state, state);
|
||||
assert_eq!(submenu.toggles.get_selected(), Some(&mut make_toggle(2)));
|
||||
|
||||
submenu.toggles.select(1, 0);
|
||||
submenu.on_right();
|
||||
state.select(Some(TableSelection::Cell { row: 1, col: 0 }));
|
||||
assert_eq!(submenu.toggles.state, state);
|
||||
assert_eq!(submenu.toggles.get_selected(), Some(&mut make_toggle(3)));
|
||||
submenu.on_right();
|
||||
assert_eq!(submenu.toggles.state, state);
|
||||
assert_eq!(submenu.toggles.get_selected(), Some(&mut make_toggle(3)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn submenu_slider_on_right() {
|
||||
let mut submenu = initialize_submenu(SubMenuType::Slider);
|
||||
let mut state = SliderState::LowerHover;
|
||||
assert_eq!(submenu.slider.unwrap().state, state);
|
||||
state = SliderState::UpperHover;
|
||||
submenu.on_right();
|
||||
assert_eq!(submenu.slider.unwrap().state, state);
|
||||
|
||||
submenu.slider = Some(StatefulSlider {
|
||||
state: SliderState::LowerSelected,
|
||||
lower: 10,
|
||||
upper: 11,
|
||||
..submenu.slider.unwrap()
|
||||
});
|
||||
state = SliderState::LowerSelected;
|
||||
submenu.on_right();
|
||||
assert_eq!(submenu.slider.unwrap().state, state);
|
||||
assert_eq!(submenu.slider.unwrap().lower, 11);
|
||||
assert_eq!(submenu.slider.unwrap().upper, 11);
|
||||
submenu.on_right();
|
||||
assert_eq!(submenu.slider.unwrap().state, state);
|
||||
assert_eq!(submenu.slider.unwrap().lower, 11);
|
||||
assert_eq!(submenu.slider.unwrap().upper, 11);
|
||||
|
||||
submenu.slider = Some(StatefulSlider {
|
||||
state: SliderState::UpperSelected,
|
||||
lower: 100,
|
||||
upper: 149,
|
||||
..submenu.slider.unwrap()
|
||||
});
|
||||
state = SliderState::UpperSelected;
|
||||
submenu.on_right();
|
||||
assert_eq!(submenu.slider.unwrap().state, state);
|
||||
assert_eq!(submenu.slider.unwrap().lower, 100);
|
||||
assert_eq!(submenu.slider.unwrap().upper, 150);
|
||||
submenu.on_right();
|
||||
assert_eq!(submenu.slider.unwrap().state, state);
|
||||
assert_eq!(submenu.slider.unwrap().lower, 100);
|
||||
assert_eq!(submenu.slider.unwrap().upper, 150);
|
||||
}
|
44
training_mod_tui/tests/test_toggle.rs
Normal file
44
training_mod_tui/tests/test_toggle.rs
Normal file
|
@ -0,0 +1,44 @@
|
|||
use training_mod_tui_2::Toggle;
|
||||
|
||||
#[test]
|
||||
fn toggle_serialize() {
|
||||
let t = Toggle {
|
||||
title: "Title",
|
||||
value: 5,
|
||||
max: 10,
|
||||
};
|
||||
let json = serde_json::to_string(&t).unwrap();
|
||||
assert_eq!(json, "5");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn toggle_increment() {
|
||||
let mut t = Toggle {
|
||||
title: "Title",
|
||||
value: 5,
|
||||
max: 10,
|
||||
};
|
||||
t.increment();
|
||||
assert_eq!(t.value, 6);
|
||||
t.value = 9;
|
||||
t.increment();
|
||||
assert_eq!(t.value, 10);
|
||||
t.increment();
|
||||
assert_eq!(t.value, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn toggle_decrement() {
|
||||
let mut t = Toggle {
|
||||
title: "Title",
|
||||
value: 5,
|
||||
max: 10,
|
||||
};
|
||||
t.decrement();
|
||||
assert_eq!(t.value, 4);
|
||||
t.value = 1;
|
||||
t.decrement();
|
||||
assert_eq!(t.value, 0);
|
||||
t.decrement();
|
||||
assert_eq!(t.value, 10);
|
||||
}
|
|
@ -1,12 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="RUST_MODULE" version="4">
|
||||
<component name="NewModuleRootManager" inherit-compiler-output="true">
|
||||
<exclude-output />
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/target" />
|
||||
</content>
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
Loading…
Reference in a new issue