1
0
Fork 0
mirror of https://github.com/jugeeya/UltimateTrainingModpack.git synced 2024-11-27 20:34:03 +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:
asimon-1 2023-12-02 12:02:43 -05:00 committed by GitHub
parent 3130b5ad30
commit 65f87df1e0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
48 changed files with 4922 additions and 4369 deletions

View file

@ -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"

View file

@ -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;

View file

@ -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)]

View file

@ -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;
}
}

View file

@ -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();

View file

@ -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;
}
}

View file

@ -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;
}

View file

@ -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.

View file

@ -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;

View file

@ -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

View file

@ -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

View file

@ -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");

View file

@ -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,

View file

@ -136,9 +136,10 @@ 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,
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),
}
}
}
@ -146,9 +147,10 @@ impl InputLog {
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"),
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),
}
}
}
@ -156,9 +158,10 @@ impl InputLog {
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"),
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),
}
}
}
@ -166,9 +169,10 @@ impl InputLog {
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"),
InputDisplay::SMASH => self.smash_button_icons(),
InputDisplay::RAW => self.raw_button_icons(),
InputDisplay::NONE => panic!("Invalid input display to log"),
_ => unreachable!(),
}
}
}
@ -301,7 +305,7 @@ pub fn handle_final_input_mapping(
out: *mut MappedInputs,
) {
unsafe {
if MENU.input_display == InputDisplay::None {
if MENU.input_display == InputDisplay::NONE {
return;
}

View file

@ -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

View file

@ -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

View file

@ -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 {

View file

@ -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;
}

View file

@ -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(),

View file

@ -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;

View file

@ -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 {

View file

@ -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;
}

View file

@ -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 => {}
}
}

View file

@ -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);

View file

@ -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

View file

@ -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"]

View 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(),
_ => {}
}
}
}

View 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);
}

View 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,
}

View 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) {}
}

View 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;
}
}
}

View file

@ -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,
}
}
}

View file

@ -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;

View file

@ -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);
}
}

View file

@ -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();
}
}
}

View 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::*;

View 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()
}
}

View 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)
}
}

View 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(),
}
}
}

View 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],
}
);
}

View 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]");
}

View 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);
}

View 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);
}

View 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);
}

View file

@ -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>