mirror of
https://github.com/jugeeya/UltimateTrainingModpack.git
synced 2024-11-24 02:44:17 +00:00
UI Code Refactor; Notifications; Save Defaults for Quick Menu (#461)
* Initial refactor * Full refactor * Depend only on pane creator flags * Small refactor * Small refactors; notification support * Don't push event for every quick menu change * Backend for defaults almost done * Run tests on CI * Finish save + reset defaults without confirmation * Added slider menu UI --------- Co-authored-by: xhudaman <edell.matthew@gmail.com>
This commit is contained in:
parent
f014acfb5c
commit
d5c0d636a0
15 changed files with 2077 additions and 1535 deletions
5
.github/workflows/rust.yml
vendored
5
.github/workflows/rust.yml
vendored
|
@ -9,7 +9,7 @@ on:
|
|||
|
||||
jobs:
|
||||
checker:
|
||||
name: Check, Clippy
|
||||
name: Check, Clippy, Tests
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
@ -29,6 +29,9 @@ jobs:
|
|||
run: cargo +nightly check --target=x86_64-unknown-linux-gnu
|
||||
- name: Clippy
|
||||
run: cargo +nightly clippy --all-targets --all-features --target=x86_64-unknown-linux-gnu
|
||||
- name: TUI Test
|
||||
working-directory: training_mod_tui
|
||||
run: cargo +nightly test
|
||||
plugin:
|
||||
name: Plugin NRO
|
||||
runs-on: ubuntu-latest
|
||||
|
|
|
@ -10,7 +10,7 @@ use skyline::nn::web::WebSessionBootMode;
|
|||
use skyline_web::{Background, BootDisplay, WebSession, Webpage};
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
use training_mod_consts::{MenuJsonStruct, TrainingModpackMenu};
|
||||
use training_mod_consts::MenuJsonStruct;
|
||||
|
||||
static mut FRAME_COUNTER_INDEX: usize = 0;
|
||||
pub static mut QUICK_MENU_FRAME_COUNTER_INDEX: usize = 0;
|
||||
|
@ -48,7 +48,7 @@ pub unsafe fn menu_condition(module_accessor: &mut smash::app::BattleObjectModul
|
|||
pub unsafe fn write_web_menu_file() {
|
||||
let tpl = Template::new(include_str!("../templates/menu.html")).unwrap();
|
||||
|
||||
let overall_menu = get_menu();
|
||||
let overall_menu = ui_menu(MENU);
|
||||
|
||||
let data = tpl.render(&overall_menu);
|
||||
|
||||
|
@ -67,7 +67,6 @@ const MENU_CONF_PATH: &str = "sd:/TrainingModpack/training_modpack_menu.json";
|
|||
|
||||
pub unsafe fn set_menu_from_json(message: &str) {
|
||||
let web_response = serde_json::from_str::<MenuJsonStruct>(message);
|
||||
let tui_response = serde_json::from_str::<TrainingModpackMenu>(message);
|
||||
info!("Received menu message: {message}");
|
||||
if let Ok(message_json) = web_response {
|
||||
// Includes both MENU and DEFAULTS_MENU
|
||||
|
@ -78,18 +77,7 @@ pub unsafe fn set_menu_from_json(message: &str) {
|
|||
MENU_CONF_PATH,
|
||||
serde_json::to_string_pretty(&message_json).unwrap(),
|
||||
)
|
||||
.expect("Failed to write menu settings file from web response");
|
||||
} else if let Ok(message_json) = tui_response {
|
||||
// Only includes MENU
|
||||
// From TUI
|
||||
MENU = message_json;
|
||||
|
||||
let conf = MenuJsonStruct {
|
||||
menu: MENU,
|
||||
defaults_menu: DEFAULTS_MENU,
|
||||
};
|
||||
std::fs::write(MENU_CONF_PATH, serde_json::to_string_pretty(&conf).unwrap())
|
||||
.expect("Failed to write menu settings file from quick menu response");
|
||||
.expect("Failed to write menu settings file");
|
||||
} else {
|
||||
skyline::error::show_error(
|
||||
0x70,
|
||||
|
@ -105,7 +93,6 @@ pub unsafe fn set_menu_from_json(message: &str) {
|
|||
);
|
||||
MENU.quick_menu = OnOff::On;
|
||||
}
|
||||
EVENT_QUEUE.push(Event::menu_open(message.to_string()));
|
||||
}
|
||||
|
||||
pub fn spawn_menu() {
|
||||
|
@ -127,7 +114,9 @@ pub fn spawn_menu() {
|
|||
}
|
||||
} else {
|
||||
let mut app = QUICK_MENU_APP.lock();
|
||||
*app = training_mod_tui::App::new(get_menu());
|
||||
*app = training_mod_tui::App::new(
|
||||
ui_menu(MENU),
|
||||
(ui_menu(DEFAULTS_MENU), serde_json::to_string(&DEFAULTS_MENU).unwrap()));
|
||||
drop(app);
|
||||
QUICK_MENU_ACTIVE = true;
|
||||
}
|
||||
|
@ -137,6 +126,9 @@ pub fn spawn_menu() {
|
|||
pub struct ButtonPresses {
|
||||
pub a: ButtonPress,
|
||||
pub b: ButtonPress,
|
||||
pub x: ButtonPress,
|
||||
pub r: ButtonPress,
|
||||
pub l: ButtonPress,
|
||||
pub zr: ButtonPress,
|
||||
pub zl: ButtonPress,
|
||||
pub left: ButtonPress,
|
||||
|
@ -183,6 +175,21 @@ pub static mut BUTTON_PRESSES: ButtonPresses = ButtonPresses {
|
|||
is_pressed: false,
|
||||
lockout_frames: 0,
|
||||
},
|
||||
x: ButtonPress {
|
||||
prev_frame_is_pressed: false,
|
||||
is_pressed: false,
|
||||
lockout_frames: 0,
|
||||
},
|
||||
r: ButtonPress {
|
||||
prev_frame_is_pressed: false,
|
||||
is_pressed: false,
|
||||
lockout_frames: 0,
|
||||
},
|
||||
l: ButtonPress {
|
||||
prev_frame_is_pressed: false,
|
||||
is_pressed: false,
|
||||
lockout_frames: 0,
|
||||
},
|
||||
zr: ButtonPress {
|
||||
prev_frame_is_pressed: false,
|
||||
is_pressed: false,
|
||||
|
@ -240,6 +247,15 @@ pub fn handle_get_npad_state(state: *mut NpadGcState, _controller_id: *const u32
|
|||
if (*state).Buttons & (1 << 1) > 0 {
|
||||
BUTTON_PRESSES.b.is_pressed = true;
|
||||
}
|
||||
if (*state).Buttons & (1 << 2) > 0 {
|
||||
BUTTON_PRESSES.x.is_pressed = true;
|
||||
}
|
||||
if (*state).Buttons & (1 << 6) > 0 {
|
||||
BUTTON_PRESSES.l.is_pressed = true;
|
||||
}
|
||||
if (*state).Buttons & (1 << 7) > 0 {
|
||||
BUTTON_PRESSES.r.is_pressed = true;
|
||||
}
|
||||
if (*state).Buttons & (1 << 8) > 0 {
|
||||
BUTTON_PRESSES.zl.is_pressed = true;
|
||||
}
|
||||
|
@ -270,18 +286,20 @@ pub fn handle_get_npad_state(state: *mut NpadGcState, _controller_id: *const u32
|
|||
|
||||
use lazy_static::lazy_static;
|
||||
use parking_lot::Mutex;
|
||||
use training_mod_tui::AppPage;
|
||||
|
||||
lazy_static! {
|
||||
pub static ref QUICK_MENU_APP: Mutex<training_mod_tui::App<'static>> =
|
||||
Mutex::new(training_mod_tui::App::new(unsafe { get_menu() }));
|
||||
Mutex::new(training_mod_tui::App::new(
|
||||
unsafe { ui_menu(MENU) },
|
||||
unsafe { (ui_menu(DEFAULTS_MENU), serde_json::to_string(&DEFAULTS_MENU).unwrap())}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
pub unsafe fn quick_menu_loop() {
|
||||
loop {
|
||||
std::thread::sleep(std::time::Duration::from_secs(10));
|
||||
let backend = training_mod_tui::TestBackend::new(75, 15);
|
||||
let mut terminal = training_mod_tui::Terminal::new(backend).unwrap();
|
||||
let mut json_response = String::new();
|
||||
let button_presses = &mut BUTTON_PRESSES;
|
||||
let mut received_input = true;
|
||||
loop {
|
||||
|
@ -299,24 +317,37 @@ pub unsafe fn quick_menu_loop() {
|
|||
let b_press = &mut button_presses.b;
|
||||
b_press.read_press().then(|| {
|
||||
received_input = true;
|
||||
if !app.outer_list {
|
||||
if app.page != AppPage::SUBMENU {
|
||||
app.on_b()
|
||||
} else if frame_counter::get_frame_count(QUICK_MENU_FRAME_COUNTER_INDEX) == 0
|
||||
&& !json_response.is_empty()
|
||||
{
|
||||
// Leave menu.
|
||||
QUICK_MENU_ACTIVE = false;
|
||||
set_menu_from_json(&json_response);
|
||||
let menu_json = app.get_menu_selections();
|
||||
set_menu_from_json(&menu_json);
|
||||
EVENT_QUEUE.push(Event::menu_open(menu_json.to_string()));
|
||||
}
|
||||
});
|
||||
button_presses.zl.read_press().then(|| {
|
||||
button_presses.x.read_press().then(|| {
|
||||
app.on_x();
|
||||
received_input = true;
|
||||
});
|
||||
button_presses.l.read_press().then(|| {
|
||||
app.on_l();
|
||||
received_input = true;
|
||||
});
|
||||
button_presses.zr.read_press().then(|| {
|
||||
button_presses.r.read_press().then(|| {
|
||||
app.on_r();
|
||||
received_input = true;
|
||||
});
|
||||
button_presses.zl.read_press().then(|| {
|
||||
app.on_zl();
|
||||
received_input = true;
|
||||
});
|
||||
button_presses.zr.read_press().then(|| {
|
||||
app.on_zr();
|
||||
received_input = true;
|
||||
});
|
||||
button_presses.left.read_press().then(|| {
|
||||
app.on_left();
|
||||
received_input = true;
|
||||
|
@ -335,10 +366,8 @@ pub unsafe fn quick_menu_loop() {
|
|||
});
|
||||
|
||||
if received_input {
|
||||
terminal
|
||||
.draw(|f| json_response = training_mod_tui::ui(f, app))
|
||||
.unwrap();
|
||||
received_input = false;
|
||||
set_menu_from_json(&app.get_menu_selections());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -360,6 +389,7 @@ unsafe fn spawn_web_session(session: WebSession) {
|
|||
session.exit();
|
||||
session.wait_for_exit();
|
||||
set_menu_from_json(&message_recv);
|
||||
EVENT_QUEUE.push(Event::menu_open(message_recv.to_string()));
|
||||
}
|
||||
|
||||
unsafe fn new_web_session(hidden: bool) -> WebSession {
|
||||
|
|
|
@ -35,6 +35,7 @@ use crate::menu::quick_menu_loop;
|
|||
#[cfg(feature = "web_session_preload")]
|
||||
use crate::menu::web_session_loop;
|
||||
use training_mod_consts::{MenuJsonStruct, OnOff};
|
||||
use crate::training::ui::notifications::notification;
|
||||
|
||||
fn nro_main(nro: &NroInfo<'_>) {
|
||||
if nro.module.isLoaded {
|
||||
|
@ -81,10 +82,12 @@ pub fn main() {
|
|||
info!("Initialized.");
|
||||
unsafe {
|
||||
EVENT_QUEUE.push(Event::smash_open());
|
||||
notification("Training Modpack", "Welcome!", 60);
|
||||
notification("Open Menu", "Special + Uptaunt", 120);
|
||||
notification("Save State", "Grab + Downtaunt", 120);
|
||||
notification("Load State", "Grab + Uptaunt", 120);
|
||||
}
|
||||
|
||||
training::ui_hacks::install_hooks();
|
||||
|
||||
hitbox_visualizer::hitbox_visualization();
|
||||
hazard_manager::hazard_manager();
|
||||
training::training_mods();
|
||||
|
|
|
@ -509,6 +509,7 @@ daikon_replace!(DAISY, daisy, 1);
|
|||
|
||||
// GenerateArticleForTarget for Peach/Diddy(/Link?) item creation
|
||||
static GAFT_OFFSET: usize = 0x03d40a0;
|
||||
|
||||
#[skyline::hook(offset = GAFT_OFFSET)]
|
||||
pub unsafe fn handle_generate_article_for_target(
|
||||
article_module_accessor: *mut app::BattleObjectModuleAccessor,
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
use skyline::nn::ui2d::ResColor;
|
||||
use crate::common::consts::FighterId;
|
||||
use crate::common::*;
|
||||
use crate::training::*;
|
||||
|
||||
pub static mut FRAME_ADVANTAGE: i32 = 0;
|
||||
static mut FRAME_ADVANTAGE_STR: String = String::new();
|
||||
static mut PLAYER_ACTIONABLE: bool = false;
|
||||
static mut CPU_ACTIONABLE: bool = false;
|
||||
static mut PLAYER_ACTIVE_FRAME: u32 = 0;
|
||||
|
@ -47,6 +49,19 @@ unsafe fn is_actionable(module_accessor: *mut app::BattleObjectModuleAccessor) -
|
|||
fn update_frame_advantage(new_frame_adv: i32) {
|
||||
unsafe {
|
||||
FRAME_ADVANTAGE = new_frame_adv;
|
||||
FRAME_ADVANTAGE_STR = String::new();
|
||||
FRAME_ADVANTAGE_STR.push_str(&format!("{}", FRAME_ADVANTAGE));
|
||||
ui::notifications::clear_notifications("Frame Advantage");
|
||||
ui::notifications::color_notification(
|
||||
"Frame Advantage",
|
||||
&FRAME_ADVANTAGE_STR,
|
||||
60,
|
||||
match FRAME_ADVANTAGE {
|
||||
x if x < 0 => ResColor{r: 200, g: 8, b: 8, a: 255},
|
||||
x if x == 0 => ResColor{r: 0, g: 0, b: 0, a: 255},
|
||||
_ => ResColor{r: 31, g: 198, b: 0, a: 255},
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -24,7 +24,7 @@ pub mod sdi;
|
|||
pub mod shield;
|
||||
pub mod tech;
|
||||
pub mod throw;
|
||||
pub mod ui_hacks;
|
||||
pub mod ui;
|
||||
|
||||
mod air_dodge_direction;
|
||||
mod attack_angle;
|
||||
|
@ -570,4 +570,5 @@ pub fn training_mods() {
|
|||
buff::init();
|
||||
items::init();
|
||||
tech::init();
|
||||
ui::init();
|
||||
}
|
||||
|
|
146
src/training/ui/damage.rs
Normal file
146
src/training/ui/damage.rs
Normal file
|
@ -0,0 +1,146 @@
|
|||
use crate::common::{get_player_dmg_digits, is_ready_go, is_training_mode};
|
||||
use crate::consts::FighterId;
|
||||
use skyline::nn::ui2d::*;
|
||||
use smash::ui2d::SmashPane;
|
||||
|
||||
pub unsafe fn iterate_anim_list(
|
||||
anim_transform_node: &mut AnimTransformNode,
|
||||
layout_name: Option<&str>,
|
||||
) {
|
||||
let mut curr = anim_transform_node as *mut AnimTransformNode;
|
||||
let mut _anim_idx = 0;
|
||||
while !curr.is_null() {
|
||||
// Only if valid
|
||||
if curr != (*curr).next {
|
||||
let anim_transform = (curr as *mut u64).add(2) as *mut AnimTransform;
|
||||
|
||||
parse_anim_transform(anim_transform.as_mut().unwrap(), layout_name);
|
||||
}
|
||||
|
||||
curr = (*curr).next;
|
||||
_anim_idx += 1;
|
||||
if curr == anim_transform_node as *mut AnimTransformNode || curr == (*curr).next {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub unsafe fn parse_anim_transform(anim_transform: &mut AnimTransform, layout_name: Option<&str>) {
|
||||
let res_animation_block_data_start = anim_transform.res_animation_block as u64;
|
||||
let res_animation_block = &*anim_transform.res_animation_block;
|
||||
let mut anim_cont_offsets = (res_animation_block_data_start
|
||||
+ res_animation_block.anim_cont_offsets_offset as u64)
|
||||
as *const u32;
|
||||
for _anim_cont_idx in 0..res_animation_block.anim_cont_count {
|
||||
let anim_cont_offset = *anim_cont_offsets;
|
||||
let res_animation_cont = (res_animation_block_data_start + anim_cont_offset as u64)
|
||||
as *const ResAnimationContent;
|
||||
|
||||
let name = skyline::try_from_c_str((*res_animation_cont).name.as_ptr())
|
||||
.unwrap_or("UNKNOWN".to_string());
|
||||
let anim_type = (*res_animation_cont).anim_content_type;
|
||||
|
||||
// AnimContentType 1 == MATERIAL
|
||||
if name.starts_with("set_dmg_num") && anim_type == 1 {
|
||||
if let Some(layout_name) = layout_name {
|
||||
let (hundreds, tens, ones, dec) = get_player_dmg_digits(match layout_name {
|
||||
"p1" => FighterId::Player,
|
||||
"p2" => FighterId::CPU,
|
||||
_ => panic!("Unknown layout name: {}", layout_name),
|
||||
});
|
||||
|
||||
if name == "set_dmg_num_3" {
|
||||
anim_transform.frame = hundreds as f32;
|
||||
}
|
||||
if name == "set_dmg_num_2" {
|
||||
anim_transform.frame = tens as f32;
|
||||
}
|
||||
if name == "set_dmg_num_1" {
|
||||
anim_transform.frame = ones as f32;
|
||||
}
|
||||
if name == "set_dmg_num_dec" {
|
||||
anim_transform.frame = dec as f32;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
anim_cont_offsets = anim_cont_offsets.add(1);
|
||||
}
|
||||
}
|
||||
|
||||
pub unsafe fn draw(root_pane: &mut Pane, layout_name: &str) {
|
||||
// Update percentage display as soon as possible on death
|
||||
if is_training_mode() && is_ready_go() && layout_name == "info_melee" {
|
||||
for player_name in &["p1", "p2"] {
|
||||
if let Some(parent) = root_pane.find_pane_by_name_recursive(player_name) {
|
||||
let _p1_layout_name = skyline::from_c_str((*parent.as_parts().layout).layout_name);
|
||||
let anim_list = &mut (*parent.as_parts().layout).anim_trans_list;
|
||||
|
||||
let mut has_altered_anim_list = false;
|
||||
let (hundreds, tens, _, _) = get_player_dmg_digits(match *player_name {
|
||||
"p1" => FighterId::Player,
|
||||
"p2" => FighterId::CPU,
|
||||
_ => panic!("Unknown player name: {}", player_name),
|
||||
});
|
||||
|
||||
for dmg_num_s in &[
|
||||
"set_dmg_num_3",
|
||||
"dig_3",
|
||||
"dig_3_anim",
|
||||
"set_dmg_num_2",
|
||||
"dig_2",
|
||||
"dig_2_anim",
|
||||
"set_dmg_num_1",
|
||||
"dig_1",
|
||||
"dig_1_anim",
|
||||
"set_dmg_num_p",
|
||||
"dig_dec",
|
||||
"dig_dec_anim_00",
|
||||
"set_dmg_num_dec",
|
||||
"dig_dec_anim_01",
|
||||
"dig_0_anim",
|
||||
"set_dmg_p",
|
||||
] {
|
||||
if let Some(dmg_num) = parent.find_pane_by_name_recursive(dmg_num_s) {
|
||||
if (dmg_num_s.contains('3') && hundreds == 0)
|
||||
|| (dmg_num_s.contains('2') && hundreds == 0 && tens == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if *dmg_num_s == "set_dmg_p" {
|
||||
dmg_num.pos_y = 0.0;
|
||||
} else if *dmg_num_s == "set_dmg_num_p" {
|
||||
dmg_num.pos_y = -4.0;
|
||||
} else if *dmg_num_s == "dig_dec" {
|
||||
dmg_num.pos_y = -16.0;
|
||||
} else {
|
||||
dmg_num.pos_y = 0.0;
|
||||
}
|
||||
|
||||
if dmg_num.alpha != 255 || dmg_num.global_alpha != 255 {
|
||||
dmg_num.set_visible(true);
|
||||
if !has_altered_anim_list {
|
||||
iterate_anim_list(anim_list, Some(player_name));
|
||||
has_altered_anim_list = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for death_explosion_s in &[
|
||||
"set_fxui_dead1",
|
||||
"set_fxui_dead2",
|
||||
"set_fxui_dead3",
|
||||
"set_fxui_fire",
|
||||
] {
|
||||
if let Some(death_explosion) =
|
||||
parent.find_pane_by_name_recursive(death_explosion_s)
|
||||
{
|
||||
death_explosion.set_visible(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
147
src/training/ui/display.rs
Normal file
147
src/training/ui/display.rs
Normal file
|
@ -0,0 +1,147 @@
|
|||
use crate::training::ui;
|
||||
use skyline::nn::ui2d::*;
|
||||
use smash::ui2d::{SmashPane, SmashTextBox};
|
||||
|
||||
pub static NUM_DISPLAY_PANES: usize = 1;
|
||||
|
||||
macro_rules! display_parent_fmt {
|
||||
($x:ident) => {
|
||||
format!("trMod_disp_{}", $x).as_str()
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! display_pic_fmt {
|
||||
($x:ident) => {
|
||||
format!("trMod_disp_{}_base", $x).as_str()
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! display_header_fmt {
|
||||
($x:ident) => {
|
||||
format!("trMod_disp_{}_header", $x).as_str()
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! display_txt_fmt {
|
||||
($x:ident) => {
|
||||
format!("trMod_disp_{}_txt", $x).as_str()
|
||||
};
|
||||
}
|
||||
|
||||
pub unsafe fn draw(root_pane: &mut Pane) {
|
||||
let notification_idx = 0;
|
||||
|
||||
let queue = &mut ui::notifications::QUEUE;
|
||||
let notification = queue.first_mut();
|
||||
|
||||
if let Some(parent) = root_pane.find_pane_by_name_recursive(display_parent_fmt!(notification_idx)) {
|
||||
parent.set_visible(notification.is_some());
|
||||
if notification.is_none() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let notification = notification.unwrap();
|
||||
let header_txt = notification.header();
|
||||
let message = notification.message();
|
||||
let color = notification.color();
|
||||
let has_completed = notification.tick();
|
||||
if has_completed {
|
||||
queue.remove(0);
|
||||
}
|
||||
|
||||
if let Some(header) = root_pane.find_pane_by_name_recursive(display_header_fmt!(notification_idx)) {
|
||||
header.as_textbox().set_text_string(header_txt);
|
||||
}
|
||||
|
||||
if let Some(text) = root_pane.find_pane_by_name_recursive(display_txt_fmt!(notification_idx)) {
|
||||
let text = text.as_textbox();
|
||||
text.set_text_string(message);
|
||||
text.set_color(color.r, color.g, color.b, color.a);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub static BUILD_PIC_BASE: ui::PaneCreationCallback = |_, root_pane, original_build, layout, out_build_result_information, device, block, parts_build_data_set, build_arg_set, build_res_set, kind| unsafe {
|
||||
macro_rules! build {
|
||||
($block: ident, $resTyp: ty, $kind:ident, $typ: ty) => {
|
||||
paste::paste! {
|
||||
&mut *(original_build(layout, out_build_result_information, device, &mut $block as *mut $resTyp as *mut ResPane, parts_build_data_set, build_arg_set, build_res_set, $kind,) as *mut $typ)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
(0..NUM_DISPLAY_PANES).for_each(|idx| {
|
||||
let block = block as *mut ResPictureWithTex<1>;
|
||||
let mut pic_block = *block;
|
||||
pic_block.set_name(display_pic_fmt!(idx));
|
||||
pic_block.set_pos(ResVec3::default());
|
||||
let pic_pane = build!(pic_block, ResPictureWithTex<1>, kind, Picture);
|
||||
pic_pane.detach();
|
||||
|
||||
// pic is loaded first, we can create our parent pane here.
|
||||
let disp_pane_kind = u32::from_le_bytes([b'p', b'a', b'n', b'1']);
|
||||
let mut disp_pane_block = ResPane::new(display_parent_fmt!(idx));
|
||||
disp_pane_block.set_pos(ResVec3::new(806.0, -50.0 - (idx as f32 * 110.0), 0.0));
|
||||
let disp_pane = build!(disp_pane_block, ResPane, disp_pane_kind, Pane);
|
||||
disp_pane.detach();
|
||||
root_pane.append_child(disp_pane);
|
||||
disp_pane.append_child(pic_pane);
|
||||
});
|
||||
};
|
||||
|
||||
pub static BUILD_PANE_TXT: ui::PaneCreationCallback = |_, root_pane, original_build, layout, out_build_result_information, device, block, parts_build_data_set, build_arg_set, build_res_set, kind| unsafe {
|
||||
macro_rules! build {
|
||||
($block: ident, $resTyp: ty, $kind:ident, $typ: ty) => {
|
||||
paste::paste! {
|
||||
&mut *(original_build(layout, out_build_result_information, device, &mut $block as *mut $resTyp as *mut ResPane, parts_build_data_set, build_arg_set, build_res_set, $kind,) as *mut $typ)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
(0..NUM_DISPLAY_PANES).for_each(|idx| {
|
||||
let disp_pane = root_pane
|
||||
.find_pane_by_name(display_parent_fmt!(idx), true)
|
||||
.unwrap();
|
||||
|
||||
let block = block as *mut ResTextBox;
|
||||
let mut text_block = *block;
|
||||
text_block.set_name(display_txt_fmt!(idx));
|
||||
text_block.set_pos(ResVec3::new(-10.0, -25.0, 0.0));
|
||||
let text_pane = build!(text_block, ResTextBox, kind, TextBox);
|
||||
text_pane.set_text_string(format!("Pane {idx}!").as_str());
|
||||
// Ensure Material Colors are not hardcoded so we can just use SetTextColor.
|
||||
text_pane.set_default_material_colors();
|
||||
text_pane.detach();
|
||||
disp_pane.append_child(text_pane);
|
||||
});
|
||||
};
|
||||
|
||||
pub static BUILD_HEADER_TXT: ui::PaneCreationCallback = |_, root_pane, original_build, layout, out_build_result_information, device, block, parts_build_data_set, build_arg_set, build_res_set, kind| unsafe {
|
||||
macro_rules! build {
|
||||
($block: ident, $resTyp: ty, $kind:ident, $typ: ty) => {
|
||||
paste::paste! {
|
||||
&mut *(original_build(layout, out_build_result_information, device, &mut $block as *mut $resTyp as *mut ResPane, parts_build_data_set, build_arg_set, build_res_set, $kind,) as *mut $typ)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
(0..NUM_DISPLAY_PANES).for_each(|idx| {
|
||||
let disp_pane = root_pane
|
||||
.find_pane_by_name(display_parent_fmt!(idx), true)
|
||||
.unwrap();
|
||||
|
||||
let block = block as *mut ResTextBox;
|
||||
let mut header_block = *block;
|
||||
header_block.set_name(display_header_fmt!(idx));
|
||||
header_block.set_pos(ResVec3::new(0.0, 25.0, 0.0));
|
||||
let header_pane = build!(header_block, ResTextBox, kind, TextBox);
|
||||
header_pane.set_text_string(format!("Header {idx}").as_str());
|
||||
// Ensure Material Colors are not hardcoded so we can just use SetTextColor.
|
||||
header_pane.set_default_material_colors();
|
||||
// Header should be white text
|
||||
header_pane.set_color(255, 255, 255, 255);
|
||||
header_pane.detach();
|
||||
disp_pane.append_child(header_pane);
|
||||
});
|
||||
};
|
973
src/training/ui/menu.rs
Normal file
973
src/training/ui/menu.rs
Normal file
|
@ -0,0 +1,973 @@
|
|||
use crate::{common::menu::QUICK_MENU_ACTIVE};
|
||||
use skyline::nn::ui2d::*;
|
||||
use smash::ui2d::{SmashPane, SmashTextBox};
|
||||
use training_mod_tui::AppPage;
|
||||
use training_mod_tui::gauge::GaugeState;
|
||||
use crate::training::ui;
|
||||
|
||||
pub static NUM_MENU_TEXT_OPTIONS: usize = 27;
|
||||
pub static NUM_MENU_TEXT_SLIDERS: usize = 2;
|
||||
pub static NUM_MENU_TABS: usize = 3;
|
||||
|
||||
pub static mut HAS_SORTED_MENU_CHILDREN: bool = false;
|
||||
|
||||
const BG_LEFT_ON_WHITE_COLOR: ResColor = ResColor {
|
||||
r: 0,
|
||||
g: 28,
|
||||
b: 118,
|
||||
a: 255,
|
||||
};
|
||||
|
||||
const BG_LEFT_ON_BLACK_COLOR: ResColor = ResColor {
|
||||
r: 0,
|
||||
g: 22,
|
||||
b: 112,
|
||||
a: 0,
|
||||
};
|
||||
|
||||
const BG_LEFT_OFF_WHITE_COLOR: ResColor = ResColor {
|
||||
r: 8,
|
||||
g: 13,
|
||||
b: 17,
|
||||
a: 255,
|
||||
};
|
||||
|
||||
const BG_LEFT_OFF_BLACK_COLOR: ResColor = ResColor {
|
||||
r: 5,
|
||||
g: 10,
|
||||
b: 14,
|
||||
a: 0,
|
||||
};
|
||||
|
||||
const BG_LEFT_SELECTED_BLACK_COLOR: ResColor = ResColor {
|
||||
r: 240,
|
||||
g: 154,
|
||||
b: 7,
|
||||
a: 0,
|
||||
};
|
||||
|
||||
const BG_LEFT_SELECTED_WHITE_COLOR: ResColor = ResColor {
|
||||
r: 255,
|
||||
g: 166,
|
||||
b: 7,
|
||||
a: 255,
|
||||
};
|
||||
|
||||
const BLACK: ResColor = ResColor {
|
||||
r: 0,
|
||||
g: 0,
|
||||
b: 0,
|
||||
a: 255,
|
||||
};
|
||||
|
||||
macro_rules! menu_text_name_fmt {
|
||||
($x:ident, $y:ident) => {
|
||||
format!("trMod_menu_opt_{}_{}", $x, $y).as_str()
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! menu_text_check_fmt {
|
||||
($x:ident, $y:ident) => {
|
||||
format!("trMod_menu_check_{}_{}", $x, $y).as_str()
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! menu_text_bg_left_fmt {
|
||||
($x:ident, $y:ident) => {
|
||||
format!("trMod_menu_bg_left_{}_{}", $x, $y).as_str()
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! menu_text_bg_back_fmt {
|
||||
($x:ident, $y:ident) => {
|
||||
format!("trMod_menu_bg_back_{}_{}", $x, $y).as_str()
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! menu_text_slider_fmt {
|
||||
($x:ident) => {
|
||||
format!("trMod_menu_slider_{}", $x).as_str()
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! menu_slider_label_fmt {
|
||||
($x:ident) => {
|
||||
format!("trMod_menu_slider_{}_lbl", $x).as_str()
|
||||
};
|
||||
}
|
||||
|
||||
// Sort all panes in under menu pane such that text and check options
|
||||
// are last
|
||||
pub unsafe fn all_menu_panes_sorted(root_pane: &Pane) -> Vec<&mut Pane> {
|
||||
let mut panes = (0..NUM_MENU_TEXT_OPTIONS)
|
||||
.flat_map(|idx| {
|
||||
let x = idx % 3;
|
||||
let y = idx / 3;
|
||||
[
|
||||
root_pane
|
||||
.find_pane_by_name_recursive(menu_text_name_fmt!(x, y))
|
||||
.unwrap(),
|
||||
root_pane
|
||||
.find_pane_by_name_recursive(menu_text_check_fmt!(x, y))
|
||||
.unwrap(),
|
||||
root_pane
|
||||
.find_pane_by_name_recursive(menu_text_bg_left_fmt!(x, y))
|
||||
.unwrap(),
|
||||
root_pane
|
||||
.find_pane_by_name_recursive(menu_text_bg_back_fmt!(x, y))
|
||||
.unwrap(),
|
||||
]
|
||||
})
|
||||
.collect::<Vec<&mut Pane>>();
|
||||
|
||||
panes.append(
|
||||
&mut (0..NUM_MENU_TEXT_SLIDERS)
|
||||
.map(|idx| {
|
||||
root_pane
|
||||
.find_pane_by_name_recursive(menu_text_slider_fmt!(idx))
|
||||
.unwrap()
|
||||
})
|
||||
.collect::<Vec<&mut Pane>>(),
|
||||
);
|
||||
|
||||
panes.append(
|
||||
&mut (0..NUM_MENU_TEXT_SLIDERS)
|
||||
.map(|idx| {
|
||||
root_pane
|
||||
.find_pane_by_name_recursive(menu_slider_label_fmt!(idx))
|
||||
.unwrap()
|
||||
})
|
||||
.collect::<Vec<&mut Pane>>(),
|
||||
);
|
||||
|
||||
panes.sort_by(|a, _| {
|
||||
if a.get_name().contains("opt") || a.get_name().contains("check") {
|
||||
std::cmp::Ordering::Greater
|
||||
} else {
|
||||
std::cmp::Ordering::Less
|
||||
}
|
||||
});
|
||||
|
||||
panes
|
||||
}
|
||||
|
||||
pub unsafe fn draw(root_pane: &mut Pane) {
|
||||
// Update menu display
|
||||
// Grabbing lock as read-only, essentially
|
||||
let app = &*crate::common::menu::QUICK_MENU_APP.data_ptr();
|
||||
|
||||
if let Some(quit_button) = root_pane.find_pane_by_name_recursive("btn_finish") {
|
||||
// Normally at (-804, 640)
|
||||
// Comes down to (-804, 514)
|
||||
if QUICK_MENU_ACTIVE {
|
||||
quit_button.pos_y = 514.0;
|
||||
}
|
||||
|
||||
for quit_txt_s in &["set_txt_00", "set_txt_01"] {
|
||||
if let Some(quit_txt) = quit_button.find_pane_by_name_recursive(quit_txt_s) {
|
||||
quit_txt.as_textbox().set_text_string(if QUICK_MENU_ACTIVE {
|
||||
"Modpack Menu"
|
||||
} else {
|
||||
// Awkward. We should get the o.g. translation for non-english games
|
||||
// Or create our own textbox here so we don't step on their toes.
|
||||
"Quit Training"
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let menu_pane = root_pane.find_pane_by_name_recursive("trMod_menu").unwrap();
|
||||
menu_pane.set_visible(QUICK_MENU_ACTIVE);
|
||||
|
||||
if !HAS_SORTED_MENU_CHILDREN {
|
||||
let sorted_panes = all_menu_panes_sorted(root_pane);
|
||||
// Place in sorted order such that backings are behind, etc.
|
||||
sorted_panes.iter().for_each(|p| menu_pane.remove_child(p));
|
||||
sorted_panes.iter().for_each(|p| menu_pane.append_child(p));
|
||||
|
||||
HAS_SORTED_MENU_CHILDREN = true;
|
||||
}
|
||||
|
||||
// Make all invisible first
|
||||
(0..NUM_MENU_TEXT_OPTIONS).for_each(|idx| {
|
||||
let x = idx % 3;
|
||||
let y = idx / 3;
|
||||
root_pane
|
||||
.find_pane_by_name_recursive(menu_text_name_fmt!(x, y))
|
||||
.map(|text| text.set_visible(false));
|
||||
root_pane
|
||||
.find_pane_by_name_recursive(menu_text_check_fmt!(x, y))
|
||||
.map(|text| text.set_visible(false));
|
||||
root_pane
|
||||
.find_pane_by_name_recursive(menu_text_bg_left_fmt!(x, y))
|
||||
.map(|text| text.set_visible(false));
|
||||
root_pane
|
||||
.find_pane_by_name_recursive(menu_text_bg_back_fmt!(x, y))
|
||||
.map(|text| text.set_visible(false));
|
||||
});
|
||||
(0..NUM_MENU_TEXT_SLIDERS).for_each(|idx| {
|
||||
root_pane
|
||||
.find_pane_by_name_recursive(menu_text_slider_fmt!(idx))
|
||||
.map(|text| text.set_visible(false));
|
||||
|
||||
root_pane
|
||||
.find_pane_by_name_recursive(menu_slider_label_fmt!(idx))
|
||||
.map(|text| text.set_visible(false));
|
||||
});
|
||||
|
||||
root_pane
|
||||
.find_pane_by_name_recursive("slider_menu")
|
||||
.map(|pane| pane.set_visible(false));
|
||||
|
||||
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]);
|
||||
|
||||
(0..NUM_MENU_TABS).for_each(|idx| {
|
||||
root_pane
|
||||
.find_pane_by_name_recursive(format!("trMod_menu_tab_{idx}").as_str())
|
||||
.map(|text| text.as_textbox().set_text_string(tab_titles[idx]));
|
||||
});
|
||||
|
||||
if app.page == AppPage::SUBMENU {
|
||||
let tab_selected = app.tab_selected();
|
||||
let tab = app.menu_items.get(tab_selected).unwrap();
|
||||
|
||||
(0..NUM_MENU_TEXT_OPTIONS)
|
||||
// Valid options in this submenu
|
||||
.filter_map(|idx| tab.idx_to_list_idx_opt(idx))
|
||||
.map(|(list_section, list_idx)| {
|
||||
(
|
||||
list_section,
|
||||
list_idx,
|
||||
root_pane
|
||||
.find_pane_by_name_recursive(menu_text_name_fmt!(
|
||||
list_section,
|
||||
list_idx
|
||||
))
|
||||
.unwrap(),
|
||||
root_pane
|
||||
.find_pane_by_name_recursive(menu_text_bg_left_fmt!(
|
||||
list_section,
|
||||
list_idx
|
||||
))
|
||||
.unwrap(),
|
||||
root_pane
|
||||
.find_pane_by_name_recursive(menu_text_bg_back_fmt!(
|
||||
list_section,
|
||||
list_idx
|
||||
))
|
||||
.unwrap(),
|
||||
)
|
||||
})
|
||||
.for_each(|(list_section, list_idx, text, bg_left, bg_back)| {
|
||||
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();
|
||||
let text = text.as_textbox();
|
||||
text.set_text_string(submenu.submenu_title);
|
||||
text.set_visible(true);
|
||||
let bg_left_material = &mut *bg_left.as_picture().material;
|
||||
if is_selected {
|
||||
if let Some(footer) =
|
||||
root_pane.find_pane_by_name_recursive("trMod_menu_footer_txt")
|
||||
{
|
||||
footer.as_textbox().set_text_string(submenu.help_text);
|
||||
}
|
||||
bg_left_material.set_white_res_color(BG_LEFT_ON_WHITE_COLOR);
|
||||
bg_left_material.set_black_res_color(BG_LEFT_ON_BLACK_COLOR);
|
||||
} else {
|
||||
bg_left_material.set_white_res_color(BG_LEFT_OFF_WHITE_COLOR);
|
||||
bg_left_material.set_black_res_color(BG_LEFT_OFF_BLACK_COLOR);
|
||||
}
|
||||
|
||||
bg_left.set_visible(true);
|
||||
bg_back.set_visible(true);
|
||||
});
|
||||
} else if matches!(app.selected_sub_menu_slider.state, GaugeState::None) {
|
||||
let (_title, _help_text, mut sub_menu_str_lists) = app.sub_menu_strs_and_states();
|
||||
(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(|(idx, (checked, name))| {
|
||||
let is_selected = sub_menu_state.selected().filter(|s| *s == idx).is_some();
|
||||
if let Some(text) = root_pane
|
||||
.find_pane_by_name_recursive(menu_text_name_fmt!(list_section, idx))
|
||||
{
|
||||
let text = text.as_textbox();
|
||||
text.set_text_string(name);
|
||||
text.set_visible(true);
|
||||
}
|
||||
|
||||
if let Some(bg_left) = root_pane
|
||||
.find_pane_by_name_recursive(menu_text_bg_left_fmt!(list_section, idx))
|
||||
{
|
||||
let bg_left_material = &mut *bg_left.as_picture().material;
|
||||
if is_selected {
|
||||
bg_left_material.set_white_res_color(BG_LEFT_ON_WHITE_COLOR);
|
||||
bg_left_material.set_black_res_color(BG_LEFT_ON_BLACK_COLOR);
|
||||
} else {
|
||||
bg_left_material.set_white_res_color(BG_LEFT_OFF_WHITE_COLOR);
|
||||
bg_left_material.set_black_res_color(BG_LEFT_OFF_BLACK_COLOR);
|
||||
}
|
||||
bg_left.set_visible(true);
|
||||
}
|
||||
|
||||
if let Some(bg_back) = root_pane
|
||||
.find_pane_by_name_recursive(menu_text_bg_back_fmt!(list_section, idx))
|
||||
{
|
||||
bg_back.set_visible(true);
|
||||
}
|
||||
|
||||
if let Some(check) = root_pane
|
||||
.find_pane_by_name_recursive(menu_text_check_fmt!(list_section, idx))
|
||||
{
|
||||
if *checked {
|
||||
let check = check.as_textbox();
|
||||
|
||||
check.set_text_string("+");
|
||||
check.set_visible(true);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
} else {
|
||||
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;
|
||||
|
||||
if let Some(pane) = root_pane.find_pane_by_name_recursive("slider_menu") {
|
||||
pane.set_visible(true);
|
||||
}
|
||||
|
||||
if let Some(text) = root_pane.find_pane_by_name_recursive("slider_title") {
|
||||
let text = text.as_textbox();
|
||||
text.set_text_string(&format!("{_title}"));
|
||||
}
|
||||
|
||||
(0..NUM_MENU_TEXT_SLIDERS).for_each(|index| {
|
||||
if let Some(text_pane) = root_pane.find_pane_by_name_recursive(
|
||||
format!("trMod_menu_slider_{}_lbl", index).as_str(),
|
||||
) {
|
||||
let text_pane = text_pane.as_textbox();
|
||||
text_pane.set_visible(true);
|
||||
|
||||
match index {
|
||||
0 => {
|
||||
text_pane.set_text_string("Min");
|
||||
|
||||
match gauge_vals.state {
|
||||
GaugeState::MinHover | GaugeState::MinSelected => {
|
||||
text_pane.m_bits |= 1 << TextBoxFlag::ShadowEnabled as u8;
|
||||
text_pane.m_bits = text_pane.m_bits & !(1 << TextBoxFlag::InvisibleBorderEnabled as u8);
|
||||
text_pane.set_color(255, 255, 255, 255);
|
||||
}
|
||||
_ => {
|
||||
text_pane.m_bits = text_pane.m_bits & !(1 << TextBoxFlag::ShadowEnabled as u8);
|
||||
text_pane.m_bits |= 1 << TextBoxFlag::InvisibleBorderEnabled as u8;
|
||||
text_pane.set_color(85, 89, 92, 255);
|
||||
}
|
||||
}
|
||||
}
|
||||
1 => {
|
||||
text_pane.set_text_string("Max");
|
||||
|
||||
match gauge_vals.state {
|
||||
GaugeState::MaxHover | GaugeState::MaxSelected => {
|
||||
text_pane.m_bits |= 1 << TextBoxFlag::ShadowEnabled as u8;
|
||||
text_pane.m_bits = text_pane.m_bits & !(1 << TextBoxFlag::InvisibleBorderEnabled as u8);
|
||||
text_pane.set_color(255, 255, 255, 255);
|
||||
}
|
||||
_ => {
|
||||
text_pane.m_bits |= 1 << TextBoxFlag::InvisibleBorderEnabled as u8;
|
||||
text_pane.m_bits = text_pane.m_bits & !(1 << TextBoxFlag::ShadowEnabled as u8);
|
||||
text_pane.set_color(85, 89, 92, 255);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => panic!("Unexpected slider label index {}!", index),
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(text_pane) = root_pane
|
||||
.find_pane_by_name_recursive(format!("trMod_menu_slider_{}", index).as_str())
|
||||
{
|
||||
let text_pane = text_pane.as_textbox();
|
||||
text_pane.set_visible(true);
|
||||
|
||||
match index {
|
||||
0 => text_pane.set_text_string(&format!("{selected_min}")),
|
||||
1 => text_pane.set_text_string(&format!("{selected_max}")),
|
||||
_ => panic!("Unexpected slider label index {}!", index),
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(bg_left) = root_pane
|
||||
.find_pane_by_name_recursive(format!("slider_btn_fg_{}", index).as_str())
|
||||
{
|
||||
let bg_left_material = &mut *bg_left.as_picture().material;
|
||||
|
||||
match index {
|
||||
0 => match gauge_vals.state {
|
||||
GaugeState::MinHover => {
|
||||
bg_left_material.set_white_res_color(BG_LEFT_ON_WHITE_COLOR);
|
||||
bg_left_material.set_black_res_color(BG_LEFT_ON_BLACK_COLOR);
|
||||
}
|
||||
GaugeState::MinSelected => {
|
||||
bg_left_material.set_white_res_color(BG_LEFT_SELECTED_WHITE_COLOR);
|
||||
bg_left_material.set_black_res_color(BG_LEFT_SELECTED_BLACK_COLOR);
|
||||
}
|
||||
_ => {
|
||||
bg_left_material.set_white_res_color(BG_LEFT_OFF_WHITE_COLOR);
|
||||
bg_left_material.set_black_res_color(BG_LEFT_OFF_BLACK_COLOR);
|
||||
}
|
||||
},
|
||||
1 => match gauge_vals.state {
|
||||
GaugeState::MaxHover => {
|
||||
bg_left_material.set_white_res_color(BG_LEFT_ON_WHITE_COLOR);
|
||||
bg_left_material.set_black_res_color(BG_LEFT_ON_BLACK_COLOR);
|
||||
}
|
||||
GaugeState::MaxSelected => {
|
||||
bg_left_material.set_white_res_color(BG_LEFT_SELECTED_WHITE_COLOR);
|
||||
bg_left_material.set_black_res_color(BG_LEFT_SELECTED_BLACK_COLOR);
|
||||
}
|
||||
_ => {
|
||||
bg_left_material.set_white_res_color(BG_LEFT_OFF_WHITE_COLOR);
|
||||
bg_left_material.set_black_res_color(BG_LEFT_OFF_BLACK_COLOR);
|
||||
}
|
||||
},
|
||||
_ => panic!("Unexpected slider label index {}!", index),
|
||||
}
|
||||
bg_left.set_visible(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub static mut MENU_PANE_PTR: u64 = 0;
|
||||
const MENU_POS : ResVec3 = ResVec3 {
|
||||
x: -360.0,
|
||||
y: 440.0,
|
||||
z: 0.0
|
||||
};
|
||||
|
||||
pub static BUILD_CONTAINER_PANE: ui::PaneCreationCallback = |_, root_pane, original_build, layout, out_build_result_information, device, _block, parts_build_data_set, build_arg_set, build_res_set, _kind| unsafe {
|
||||
macro_rules! build {
|
||||
($block: ident, $resTyp: ty, $kind:ident, $typ: ty) => {
|
||||
paste::paste! {
|
||||
&mut *(original_build(layout, out_build_result_information, device, &mut $block as *mut $resTyp as *mut ResPane, parts_build_data_set, build_arg_set, build_res_set, $kind,) as *mut $typ)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Let's create our parent display pane here.
|
||||
let menu_pane_kind = u32::from_le_bytes([b'p', b'a', b'n', b'1']);
|
||||
let mut menu_pane_block = ResPane::new("trMod_menu");
|
||||
// Overall menu pane @ 0,0 to reason about positions globally
|
||||
menu_pane_block.set_pos(ResVec3::default());
|
||||
let menu_pane = build!(menu_pane_block, ResPane, menu_pane_kind, Pane);
|
||||
menu_pane.detach();
|
||||
|
||||
root_pane.append_child(menu_pane);
|
||||
if MENU_PANE_PTR != menu_pane as *mut Pane as u64 {
|
||||
MENU_PANE_PTR = menu_pane as *mut Pane as u64;
|
||||
HAS_SORTED_MENU_CHILDREN = false;
|
||||
}
|
||||
|
||||
ui::reset_creation();
|
||||
};
|
||||
|
||||
pub static BUILD_FOOTER_BG: ui::PaneCreationCallback = |_, root_pane, original_build, layout, out_build_result_information, device, block, parts_build_data_set, build_arg_set, build_res_set, kind| unsafe {
|
||||
macro_rules! build {
|
||||
($block: ident, $resTyp: ty, $kind:ident, $typ: ty) => {
|
||||
paste::paste! {
|
||||
&mut *(original_build(layout, out_build_result_information, device, &mut $block as *mut $resTyp as *mut ResPane, parts_build_data_set, build_arg_set, build_res_set, $kind,) as *mut $typ)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
let menu_pane = root_pane.find_pane_by_name("trMod_menu", true).unwrap();
|
||||
let block = block as *mut ResPictureWithTex<1>;
|
||||
// For menu backing
|
||||
let mut pic_menu_block = *block;
|
||||
pic_menu_block.set_name("trMod_menu_footer_bg");
|
||||
let pic_menu_pane = build!(pic_menu_block, ResPictureWithTex<1>, kind, Picture);
|
||||
pic_menu_pane.detach();
|
||||
|
||||
menu_pane.append_child(pic_menu_pane);
|
||||
};
|
||||
|
||||
pub static BUILD_FOOTER_TXT: ui::PaneCreationCallback = |_, root_pane, original_build, layout, out_build_result_information, device, block, parts_build_data_set, build_arg_set, build_res_set, kind| unsafe {
|
||||
macro_rules! build {
|
||||
($block: ident, $resTyp: ty, $kind:ident, $typ: ty) => {
|
||||
paste::paste! {
|
||||
&mut *(original_build(layout, out_build_result_information, device, &mut $block as *mut $resTyp as *mut ResPane, parts_build_data_set, build_arg_set, build_res_set, $kind,) as *mut $typ)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
let menu_pane = root_pane.find_pane_by_name("trMod_menu", true).unwrap();
|
||||
|
||||
let block = block as *mut ResTextBox;
|
||||
let mut text_block = *block;
|
||||
text_block.set_name("trMod_menu_footer_txt");
|
||||
|
||||
let text_pane = build!(text_block, ResTextBox, kind, TextBox);
|
||||
text_pane.set_text_string("Footer!");
|
||||
// Ensure Material Colors are not hardcoded so we can just use SetTextColor.
|
||||
text_pane.set_default_material_colors();
|
||||
text_pane.set_color(255, 255, 255, 255);
|
||||
text_pane.detach();
|
||||
|
||||
menu_pane.append_child(text_pane);
|
||||
};
|
||||
|
||||
pub static BUILD_TAB_TXTS: ui::PaneCreationCallback = |_, root_pane, original_build, layout, out_build_result_information, device, block, parts_build_data_set, build_arg_set, build_res_set, kind| unsafe {
|
||||
macro_rules! build {
|
||||
($block: ident, $resTyp: ty, $kind:ident, $typ: ty) => {
|
||||
paste::paste! {
|
||||
&mut *(original_build(layout, out_build_result_information, device, &mut $block as *mut $resTyp as *mut ResPane, parts_build_data_set, build_arg_set, build_res_set, $kind,) as *mut $typ)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
(0..NUM_MENU_TABS).for_each(|txt_idx| {
|
||||
let menu_pane = root_pane.find_pane_by_name("trMod_menu", true).unwrap();
|
||||
|
||||
let block = block as *mut ResTextBox;
|
||||
let mut text_block = *block;
|
||||
text_block.enable_shadow();
|
||||
text_block.text_alignment(TextAlignment::Center);
|
||||
|
||||
let x = txt_idx;
|
||||
text_block.set_name(format!("trMod_menu_tab_{x}").as_str());
|
||||
|
||||
let mut x_offset = x as f32 * 300.0;
|
||||
// Center current tab since we don't have a help key
|
||||
if x == 1 {
|
||||
x_offset -= 25.0;
|
||||
}
|
||||
text_block.set_pos(ResVec3::new(
|
||||
MENU_POS.x - 25.0 + x_offset,
|
||||
MENU_POS.y + 75.0,
|
||||
0.0,
|
||||
));
|
||||
let text_pane = build!(text_block, ResTextBox, kind, TextBox);
|
||||
text_pane.set_text_string(format!("Tab {txt_idx}!").as_str());
|
||||
// Ensure Material Colors are not hardcoded so we can just use SetTextColor.
|
||||
text_pane.set_default_material_colors();
|
||||
text_pane.set_color(255, 255, 255, 255);
|
||||
if txt_idx == 1 {
|
||||
text_pane.set_color(255, 255, 0, 255);
|
||||
}
|
||||
text_pane.detach();
|
||||
menu_pane.append_child(text_pane);
|
||||
|
||||
let mut help_block = *block;
|
||||
// Font Idx 2 = nintendo64 which contains nice symbols
|
||||
help_block.font_idx = 2;
|
||||
|
||||
let x = txt_idx;
|
||||
help_block.set_name(format!("trMod_menu_tab_help_{x}").as_str());
|
||||
|
||||
let x_offset = x as f32 * 300.0;
|
||||
help_block.set_pos(ResVec3::new(
|
||||
MENU_POS.x - 250.0 + x_offset,
|
||||
MENU_POS.y + 75.0,
|
||||
0.0,
|
||||
));
|
||||
let help_pane = build!(help_block, ResTextBox, kind, TextBox);
|
||||
help_pane.set_text_string("abcd");
|
||||
let it = help_pane.m_text_buf as *mut u16;
|
||||
match txt_idx {
|
||||
// Left Tab: ZL
|
||||
0 => {
|
||||
*it = 0xE0E6;
|
||||
*(it.add(1)) = 0x0;
|
||||
help_pane.m_text_len = 1;
|
||||
}
|
||||
1 => {
|
||||
*it = 0x0;
|
||||
help_pane.m_text_len = 0;
|
||||
}
|
||||
// Right Tab: ZR
|
||||
2 => {
|
||||
*it = 0xE0E7;
|
||||
*(it.add(1)) = 0x0;
|
||||
help_pane.m_text_len = 1;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
// Ensure Material Colors are not hardcoded so we can just use SetTextColor.
|
||||
help_pane.set_default_material_colors();
|
||||
help_pane.set_color(255, 255, 255, 255);
|
||||
help_pane.detach();
|
||||
menu_pane.append_child(help_pane);
|
||||
});
|
||||
};
|
||||
|
||||
pub static BUILD_OPT_TXTS: ui::PaneCreationCallback = |_, root_pane, original_build, layout, out_build_result_information, device, block, parts_build_data_set, build_arg_set, build_res_set, kind| unsafe {
|
||||
macro_rules! build {
|
||||
($block: ident, $resTyp: ty, $kind:ident, $typ: ty) => {
|
||||
paste::paste! {
|
||||
&mut *(original_build(layout, out_build_result_information, device, &mut $block as *mut $resTyp as *mut ResPane, parts_build_data_set, build_arg_set, build_res_set, $kind,) as *mut $typ)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
(0..NUM_MENU_TEXT_OPTIONS).for_each(|txt_idx| {
|
||||
let x = txt_idx % 3;
|
||||
let y = txt_idx / 3;
|
||||
|
||||
let menu_pane = root_pane.find_pane_by_name("trMod_menu", true).unwrap();
|
||||
|
||||
let block = block as *mut ResTextBox;
|
||||
let mut text_block = *block;
|
||||
text_block.enable_shadow();
|
||||
text_block.text_alignment(TextAlignment::Center);
|
||||
|
||||
text_block.set_name(menu_text_name_fmt!(x, y));
|
||||
|
||||
let x_offset = x as f32 * 500.0;
|
||||
let y_offset = y as f32 * 85.0;
|
||||
text_block.set_pos(ResVec3::new(
|
||||
MENU_POS.x - 480.0 + x_offset,
|
||||
MENU_POS.y - 50.0 - y_offset,
|
||||
0.0,
|
||||
));
|
||||
let text_pane = build!(text_block, ResTextBox, kind, TextBox);
|
||||
text_pane.set_text_string(format!("Opt {txt_idx}!").as_str());
|
||||
// Ensure Material Colors are not hardcoded so we can just use SetTextColor.
|
||||
text_pane.set_default_material_colors();
|
||||
text_pane.set_color(255, 255, 255, 255);
|
||||
text_pane.detach();
|
||||
menu_pane.append_child(text_pane);
|
||||
|
||||
let mut check_block = *block;
|
||||
// Font Idx 2 = nintendo64 which contains nice symbols
|
||||
check_block.font_idx = 2;
|
||||
|
||||
check_block.set_name(menu_text_check_fmt!(x, y));
|
||||
check_block.set_pos(ResVec3::new(
|
||||
MENU_POS.x - 375.0 + x_offset,
|
||||
MENU_POS.y - 50.0 - y_offset,
|
||||
0.0,
|
||||
));
|
||||
let check_pane = build!(check_block, ResTextBox, kind, TextBox);
|
||||
check_pane.set_text_string(format!("Check {txt_idx}!").as_str());
|
||||
// Ensure Material Colors are not hardcoded so we can just use SetTextColor.
|
||||
check_pane.set_default_material_colors();
|
||||
check_pane.set_color(0, 0, 0, 255);
|
||||
check_pane.detach();
|
||||
menu_pane.append_child(check_pane);
|
||||
});
|
||||
};
|
||||
|
||||
pub static BUILD_SLIDER_CONTAINER_PANE: ui::PaneCreationCallback = |_, root_pane, original_build, layout, out_build_result_information, device, block, parts_build_data_set, build_arg_set, build_res_set, kind| unsafe {
|
||||
macro_rules! build {
|
||||
($block: ident, $resTyp: ty, $kind:ident, $typ: ty) => {
|
||||
paste::paste! {
|
||||
&mut *(original_build(layout, out_build_result_information, device, &mut $block as *mut $resTyp as *mut ResPane, parts_build_data_set, build_arg_set, build_res_set, $kind,) as *mut $typ)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
let slider_root_name = "slider_menu";
|
||||
let slider_container_name = "slider_ui_container";
|
||||
|
||||
let menu_pane = root_pane.find_pane_by_name("trMod_menu", true).unwrap();
|
||||
let slider_ui_root_pane_kind = u32::from_le_bytes([b'p', b'a', b'n', b'1']);
|
||||
let mut slider_ui_root_block = ResPane::new(slider_root_name);
|
||||
|
||||
slider_ui_root_block.set_pos(ResVec3::default());
|
||||
|
||||
let slider_ui_root = build!(
|
||||
slider_ui_root_block,
|
||||
ResPane,
|
||||
slider_ui_root_pane_kind,
|
||||
Pane
|
||||
);
|
||||
|
||||
slider_ui_root.detach();
|
||||
menu_pane.append_child(slider_ui_root);
|
||||
|
||||
let block = block as *mut ResPictureWithTex<1>;
|
||||
|
||||
let mut picture_block = *block;
|
||||
|
||||
picture_block.set_name(slider_container_name);
|
||||
picture_block.set_size(ResVec2::new(675.0, 300.0));
|
||||
picture_block.set_pos(ResVec3::new(-530.0, 180.0, 0.0));
|
||||
picture_block.tex_coords = [
|
||||
[ResVec2::new(0.0, 0.0)],
|
||||
[ResVec2::new(1.0, 0.0)],
|
||||
[ResVec2::new(0.0, 1.5)],
|
||||
[ResVec2::new(1.0, 1.5)],
|
||||
];
|
||||
|
||||
let picture_pane = build!(picture_block, ResPictureWithTex<1>, kind, Picture);
|
||||
picture_pane.detach();
|
||||
slider_ui_root.append_child(picture_pane);
|
||||
};
|
||||
|
||||
pub static BUILD_SLIDER_HEADER_TXT: ui::PaneCreationCallback = |_, root_pane, original_build, layout, out_build_result_information, device, block, parts_build_data_set, build_arg_set, build_res_set, kind| unsafe {
|
||||
macro_rules! build {
|
||||
($block: ident, $resTyp: ty, $kind:ident, $typ: ty) => {
|
||||
paste::paste! {
|
||||
&mut *(original_build(layout, out_build_result_information, device, &mut $block as *mut $resTyp as *mut ResPane, parts_build_data_set, build_arg_set, build_res_set, $kind,) as *mut $typ)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
let slider_root_name = "slider_menu";
|
||||
let container_pane = root_pane.find_pane_by_name(slider_root_name, true).unwrap();
|
||||
|
||||
let block = block as *mut ResTextBox;
|
||||
let mut title_block = *block;
|
||||
|
||||
title_block.set_name("slider_title");
|
||||
title_block.set_pos(ResVec3::new(-530.0, 285.0, 0.0));
|
||||
title_block.set_size(ResVec2::new(550.0, 100.0));
|
||||
title_block.font_size = ResVec2::new(50.0, 100.0);
|
||||
|
||||
let title_pane = build!(title_block, ResTextBox, kind, TextBox);
|
||||
|
||||
title_pane.set_text_string(format!("Slider Title").as_str());
|
||||
|
||||
// Ensure Material Colors are not hardcoded so we can just use SetTextColor.
|
||||
title_pane.set_default_material_colors();
|
||||
|
||||
// Header should be white text
|
||||
title_pane.set_color(255, 255, 255, 255);
|
||||
title_pane.detach();
|
||||
container_pane.append_child(title_pane);
|
||||
};
|
||||
|
||||
pub static BUILD_SLIDER_TXTS: ui::PaneCreationCallback = |_, root_pane, original_build, layout, out_build_result_information, device, block, parts_build_data_set, build_arg_set, build_res_set, kind| unsafe {
|
||||
macro_rules! build {
|
||||
($block: ident, $resTyp: ty, $kind:ident, $typ: ty) => {
|
||||
paste::paste! {
|
||||
&mut *(original_build(layout, out_build_result_information, device, &mut $block as *mut $resTyp as *mut ResPane, parts_build_data_set, build_arg_set, build_res_set, $kind,) as *mut $typ)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
let slider_root_name = "slider_menu";
|
||||
let slider_container_name = "slider_ui_container";
|
||||
|
||||
(0..NUM_MENU_TEXT_SLIDERS).for_each(|idx| {
|
||||
let x = idx % 2;
|
||||
|
||||
let label_x_offset = x as f32 * 345.0;
|
||||
|
||||
let slider_root_pane = root_pane.find_pane_by_name(slider_root_name, true).unwrap();
|
||||
let slider_container = root_pane
|
||||
.find_pane_by_name(slider_container_name, true)
|
||||
.unwrap();
|
||||
|
||||
let block = block as *mut ResTextBox;
|
||||
|
||||
let mut text_block = *block;
|
||||
|
||||
text_block.text_alignment(TextAlignment::Center);
|
||||
|
||||
text_block.set_name(menu_text_slider_fmt!(idx));
|
||||
|
||||
let value_x_offset = x as f32 * 345.0;
|
||||
|
||||
text_block.set_pos(ResVec3::new(
|
||||
slider_root_pane.pos_x - 675.0 + value_x_offset,
|
||||
slider_root_pane.pos_y + (slider_container.size_y * 0.458),
|
||||
0.0,
|
||||
));
|
||||
|
||||
let text_pane = build!(text_block, ResTextBox, kind, TextBox);
|
||||
text_pane.set_text_string(format!("Slider opt {idx}!").as_str());
|
||||
// Ensure Material Colors are not hardcoded so we can just use SetTextColor.
|
||||
text_pane.set_default_material_colors();
|
||||
text_pane.set_color(0, 0, 0, 255);
|
||||
text_pane.detach();
|
||||
slider_root_pane.append_child(text_pane);
|
||||
|
||||
let mut label_block = *block;
|
||||
|
||||
label_block.text_alignment(TextAlignment::Center);
|
||||
label_block.set_name(menu_slider_label_fmt!(idx));
|
||||
label_block.set_pos(ResVec3::new(
|
||||
slider_root_pane.pos_x - 750.0 + label_x_offset,
|
||||
slider_root_pane.pos_y + slider_container.size_y * 0.458 + 5.0,
|
||||
0.0,
|
||||
));
|
||||
label_block.font_size = ResVec2::new(25.0, 50.0);
|
||||
|
||||
// Aligns text to the center horizontally
|
||||
label_block.text_position = 4;
|
||||
|
||||
label_block.shadow_offset = ResVec2::new(4.0, -3.0);
|
||||
label_block.shadow_cols = [BLACK, BLACK];
|
||||
label_block.shadow_scale = ResVec2::new(1.0, 1.0);
|
||||
|
||||
let label_pane = build!(label_block, ResTextBox, kind, TextBox);
|
||||
|
||||
label_pane.set_text_string(format!("Slider opt {idx}!").as_str());
|
||||
// Ensure Material Colors are not hardcoded so we can just use SetTextColor.
|
||||
label_pane.set_default_material_colors();
|
||||
label_pane.set_color(85, 89, 92, 255);
|
||||
// Turns on text outline
|
||||
label_pane.m_bits = label_pane.m_bits & !(1 << TextBoxFlag::InvisibleBorderEnabled as u8);
|
||||
label_pane.detach();
|
||||
|
||||
slider_root_pane.append_child(label_pane);
|
||||
|
||||
});
|
||||
};
|
||||
|
||||
pub static BUILD_BG_LEFTS: ui::PaneCreationCallback = |_, _, original_build, layout, out_build_result_information, device, block, parts_build_data_set, build_arg_set, build_res_set, kind| unsafe {
|
||||
macro_rules! build {
|
||||
($block: ident, $resTyp: ty, $kind:ident, $typ: ty) => {
|
||||
paste::paste! {
|
||||
&mut *(original_build(layout, out_build_result_information, device, &mut $block as *mut $resTyp as *mut ResPane, parts_build_data_set, build_arg_set, build_res_set, $kind,) as *mut $typ)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
(0..NUM_MENU_TEXT_OPTIONS).for_each(|txt_idx| {
|
||||
let x = txt_idx % 3;
|
||||
let y = txt_idx / 3;
|
||||
|
||||
let x_offset = x as f32 * 500.0;
|
||||
let y_offset = y as f32 * 85.0;
|
||||
|
||||
let block = block as *mut ResPictureWithTex<2>;
|
||||
let mut pic_menu_block = *block;
|
||||
pic_menu_block.set_name(menu_text_bg_left_fmt!(x, y));
|
||||
pic_menu_block.picture.scale_x /= 1.5;
|
||||
pic_menu_block.picture.set_pos(ResVec3::new(
|
||||
MENU_POS.x - 400.0 - 195.0 + x_offset,
|
||||
MENU_POS.y - 50.0 - y_offset,
|
||||
0.0,
|
||||
));
|
||||
let pic_menu_pane = build!(pic_menu_block, ResPictureWithTex<2>, kind, Picture);
|
||||
pic_menu_pane.detach();
|
||||
if MENU_PANE_PTR != 0 {
|
||||
(*(MENU_PANE_PTR as *mut Pane)).append_child(pic_menu_pane);
|
||||
}
|
||||
});
|
||||
|
||||
(0..NUM_MENU_TEXT_SLIDERS).for_each(|index| {
|
||||
let x = index % 2;
|
||||
|
||||
if MENU_PANE_PTR != 0 {
|
||||
let slider_root = (*(MENU_PANE_PTR as *mut Pane))
|
||||
.find_pane_by_name("slider_menu", true)
|
||||
.unwrap();
|
||||
let slider_bg = (*(MENU_PANE_PTR as *mut Pane))
|
||||
.find_pane_by_name("slider_ui_container", true)
|
||||
.unwrap();
|
||||
let x_offset = x as f32 * 345.0;
|
||||
|
||||
let block = block as *mut ResPictureWithTex<2>;
|
||||
let mut pic_menu_block = *block;
|
||||
|
||||
pic_menu_block.set_name(format!("slider_btn_fg_{}", index).as_str());
|
||||
|
||||
pic_menu_block.picture.scale_x /= 1.85;
|
||||
pic_menu_block.picture.scale_y /= 1.25;
|
||||
|
||||
pic_menu_block.set_pos(ResVec3::new(
|
||||
slider_root.pos_x - 842.5 + x_offset,
|
||||
slider_root.pos_y + slider_bg.size_y * 0.458,
|
||||
0.0,
|
||||
));
|
||||
|
||||
let pic_menu_pane = build!(pic_menu_block, ResPictureWithTex<2>, kind, Picture);
|
||||
pic_menu_pane.detach();
|
||||
|
||||
slider_root.append_child(pic_menu_pane);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
pub static BUILD_BG_BACKS: ui::PaneCreationCallback = |_, _, original_build, layout, out_build_result_information, device, block, parts_build_data_set, build_arg_set, build_res_set, kind| unsafe {
|
||||
macro_rules! build {
|
||||
($block: ident, $resTyp: ty, $kind:ident, $typ: ty) => {
|
||||
paste::paste! {
|
||||
&mut *(original_build(layout, out_build_result_information, device, &mut $block as *mut $resTyp as *mut ResPane, parts_build_data_set, build_arg_set, build_res_set, $kind,) as *mut $typ)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
(0..NUM_MENU_TEXT_OPTIONS).for_each(|txt_idx| {
|
||||
let x = txt_idx % 3;
|
||||
let y = txt_idx / 3;
|
||||
|
||||
let x_offset = x as f32 * 500.0;
|
||||
let y_offset = y as f32 * 85.0;
|
||||
|
||||
let block = block as *mut ResWindowWithTexCoordsAndFrames<1, 4>;
|
||||
|
||||
let mut bg_block = *block;
|
||||
bg_block.set_name(menu_text_bg_back_fmt!(x, y));
|
||||
bg_block.scale_x /= 2.0;
|
||||
bg_block.set_pos(ResVec3::new(
|
||||
MENU_POS.x - 400.0 + x_offset,
|
||||
MENU_POS.y - 50.0 - y_offset,
|
||||
0.0,
|
||||
));
|
||||
let bg_pane = build!(bg_block, ResWindowWithTexCoordsAndFrames<1,4>, kind, Window);
|
||||
bg_pane.detach();
|
||||
if MENU_PANE_PTR != 0 {
|
||||
(*(MENU_PANE_PTR as *mut Pane)).append_child(bg_pane);
|
||||
}
|
||||
});
|
||||
|
||||
(0..NUM_MENU_TEXT_SLIDERS).for_each(|index| {
|
||||
let x = index % 2;
|
||||
|
||||
if MENU_PANE_PTR != 0 {
|
||||
let slider_root = (*(MENU_PANE_PTR as *mut Pane))
|
||||
.find_pane_by_name("slider_menu", true)
|
||||
.unwrap();
|
||||
let slider_bg = (*(MENU_PANE_PTR as *mut Pane))
|
||||
.find_pane_by_name("slider_ui_container", true)
|
||||
.unwrap();
|
||||
|
||||
let size_y = 90.0;
|
||||
|
||||
let x_offset = x as f32 * 345.0;
|
||||
|
||||
let block = block as *mut ResWindowWithTexCoordsAndFrames<1, 4>;
|
||||
let mut bg_block = *block;
|
||||
|
||||
bg_block.set_name(format!("slider_item_btn_{}", index).as_str());
|
||||
bg_block.scale_x /= 2.0;
|
||||
|
||||
bg_block.set_size(ResVec2::new(605.0, size_y));
|
||||
|
||||
bg_block.set_pos(ResVec3::new(
|
||||
slider_root.pos_x - 700.0 + x_offset,
|
||||
slider_root.pos_y + slider_bg.size_y * 0.458,
|
||||
0.0,
|
||||
));
|
||||
|
||||
let bg_pane = build!(bg_block, ResWindowWithTexCoordsAndFrames<1,4>, kind, Window);
|
||||
bg_pane.detach();
|
||||
|
||||
slider_root.append_child(bg_pane);
|
||||
}
|
||||
});
|
||||
};
|
168
src/training/ui/mod.rs
Normal file
168
src/training/ui/mod.rs
Normal file
|
@ -0,0 +1,168 @@
|
|||
use crate::common::{is_ready_go, is_training_mode};
|
||||
use skyline::nn::ui2d::*;
|
||||
use training_mod_consts::{OnOff, MENU};
|
||||
use std::collections::HashMap;
|
||||
use parking_lot::Mutex;
|
||||
use skyline::libc::c_void;
|
||||
|
||||
mod damage;
|
||||
mod display;
|
||||
mod menu;
|
||||
pub mod notifications;
|
||||
|
||||
type PaneCreationCallback = for<'a, 'b> unsafe fn(&'a str, &'b mut Pane,
|
||||
extern "C" fn(*mut Layout, *mut u8, *const u8, *mut ResPane, *const u8, *const u8, *const u8, u32) -> *mut Pane,
|
||||
*mut Layout, *mut u8, *const u8, *mut ResPane,
|
||||
*const u8, *const u8, *const u8, u32);
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
static ref PANE_CREATED: Mutex<HashMap<
|
||||
(String, String), Vec<(bool, PaneCreationCallback)>
|
||||
>> = Mutex::new(HashMap::from([
|
||||
(
|
||||
(String::from("info_training"), String::from("pic_numbase_01")),
|
||||
vec![
|
||||
(false, menu::BUILD_CONTAINER_PANE),
|
||||
(false, display::BUILD_PIC_BASE),
|
||||
(false, menu::BUILD_SLIDER_CONTAINER_PANE),
|
||||
]
|
||||
),
|
||||
(
|
||||
(String::from("info_training"), String::from("pic_help_bg_00")),
|
||||
vec![(false, menu::BUILD_FOOTER_BG)]
|
||||
),
|
||||
(
|
||||
(String::from("info_training"), String::from("set_txt_help_00")),
|
||||
vec![(false, menu::BUILD_FOOTER_TXT)]
|
||||
),
|
||||
(
|
||||
(String::from("info_training"), String::from("set_txt_num_01")),
|
||||
vec![
|
||||
(false, menu::BUILD_TAB_TXTS),
|
||||
(false, menu::BUILD_OPT_TXTS),
|
||||
(false, menu::BUILD_SLIDER_TXTS),
|
||||
(false, display::BUILD_PANE_TXT),
|
||||
]
|
||||
),
|
||||
(
|
||||
(String::from("info_training"), String::from("txt_cap_01")),
|
||||
vec![
|
||||
(false, display::BUILD_HEADER_TXT),
|
||||
(false, menu::BUILD_SLIDER_HEADER_TXT),
|
||||
]
|
||||
),
|
||||
(
|
||||
(String::from("info_training_btn0_00_item"), String::from("icn_bg_main")),
|
||||
vec![(false, menu::BUILD_BG_LEFTS)]
|
||||
),
|
||||
(
|
||||
(String::from("info_training_btn0_00_item"), String::from("btn_bg")),
|
||||
vec![(false, menu::BUILD_BG_BACKS)]
|
||||
),
|
||||
]));
|
||||
}
|
||||
|
||||
pub unsafe fn reset_creation() {
|
||||
let pane_created = &mut *PANE_CREATED.data_ptr();
|
||||
pane_created.iter_mut().for_each(|(_identifier, creators)| {
|
||||
creators.iter_mut().for_each(|(created, _callback)| {
|
||||
*created = false;
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
#[skyline::hook(offset = 0x4b620)]
|
||||
pub unsafe fn handle_draw(layout: *mut Layout, draw_info: u64, cmd_buffer: u64) {
|
||||
let layout_name = &skyline::from_c_str((*layout).layout_name);
|
||||
let root_pane = &mut *(*layout).root_pane;
|
||||
|
||||
// Set HUD to invisible if HUD is toggled off
|
||||
if is_training_mode() && is_ready_go() && layout_name != "info_training" {
|
||||
// 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);
|
||||
}
|
||||
|
||||
damage::draw(root_pane, layout_name);
|
||||
|
||||
if layout_name == "info_training" {
|
||||
display::draw(root_pane);
|
||||
menu::draw(root_pane);
|
||||
}
|
||||
|
||||
original!()(layout, draw_info, cmd_buffer);
|
||||
}
|
||||
|
||||
#[skyline::hook(offset = 0x493a0)]
|
||||
pub unsafe fn layout_build_parts_impl(
|
||||
layout: *mut Layout,
|
||||
out_build_result_information: *mut u8,
|
||||
device: *const u8,
|
||||
block: *mut ResPane,
|
||||
parts_build_data_set: *const u8,
|
||||
build_arg_set: *const u8,
|
||||
build_res_set: *const u8,
|
||||
kind: u32,
|
||||
) -> *mut Pane {
|
||||
let layout_name = &skyline::from_c_str((*layout).layout_name);
|
||||
let root_pane = &mut *(*layout).root_pane;
|
||||
|
||||
let block_name = (*block).get_name();
|
||||
let identifier = (layout_name.to_string(), block_name);
|
||||
let pane_created = &mut *PANE_CREATED.data_ptr();
|
||||
let panes = pane_created.get_mut(&identifier);
|
||||
if let Some(panes) = panes {
|
||||
panes.iter_mut().for_each(|(has_created, callback)| {
|
||||
if !*has_created {
|
||||
callback(layout_name,
|
||||
root_pane,
|
||||
original!(),
|
||||
layout,
|
||||
out_build_result_information,
|
||||
device,
|
||||
block,
|
||||
parts_build_data_set,
|
||||
build_arg_set,
|
||||
build_res_set,
|
||||
kind
|
||||
);
|
||||
|
||||
// Special case: Menu init should always occur
|
||||
if ("info_training".to_string(), "pic_numbase_01".to_string()) != identifier {
|
||||
*has_created = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
original!()(
|
||||
layout,
|
||||
out_build_result_information,
|
||||
device,
|
||||
block,
|
||||
parts_build_data_set,
|
||||
build_arg_set,
|
||||
build_res_set,
|
||||
kind,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
#[skyline::hook(offset = 0x32cace0)]
|
||||
pub unsafe fn handle_load_layout_files(
|
||||
meta_layout_root: *mut c_void,
|
||||
loading_list: *mut c_void,
|
||||
layout_arc_hash: *const u32,
|
||||
param_4: i32
|
||||
) -> u64 {
|
||||
println!("Layout.arc hash: {:x}", *layout_arc_hash);
|
||||
original!()(meta_layout_root, loading_list, layout_arc_hash, param_4)
|
||||
}
|
||||
|
||||
pub fn init() {
|
||||
skyline::install_hooks!(
|
||||
handle_draw,
|
||||
layout_build_parts_impl,
|
||||
// handle_load_layout_files
|
||||
);
|
||||
}
|
69
src/training/ui/notifications.rs
Normal file
69
src/training/ui/notifications.rs
Normal file
|
@ -0,0 +1,69 @@
|
|||
use skyline::nn::ui2d::ResColor;
|
||||
|
||||
pub static mut QUEUE: Vec<Notification<'static>> = vec![];
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct Notification<'a> {
|
||||
header: &'a str,
|
||||
message: &'a str,
|
||||
length: u32,
|
||||
color: ResColor
|
||||
}
|
||||
|
||||
impl<'a> Notification<'a> {
|
||||
pub fn new(header: &'a str, message: &'a str, length: u32, color: ResColor) -> Notification<'a> {
|
||||
Notification {
|
||||
header,
|
||||
message,
|
||||
length,
|
||||
color
|
||||
}
|
||||
}
|
||||
|
||||
// Returns: has_completed
|
||||
pub fn tick(&mut self) -> bool {
|
||||
if self.length <= 1 {
|
||||
return true;
|
||||
}
|
||||
self.length -= 1;
|
||||
false
|
||||
}
|
||||
|
||||
pub fn header(self) -> &'a str {
|
||||
self.header
|
||||
}
|
||||
|
||||
pub fn message(self) -> &'a str {
|
||||
self.message
|
||||
}
|
||||
|
||||
pub fn color(self) -> ResColor {
|
||||
self.color
|
||||
}
|
||||
}
|
||||
|
||||
pub fn notification(header: &'static str, message: &'static str, len: u32) {
|
||||
unsafe {
|
||||
let queue = &mut QUEUE;
|
||||
queue.push(Notification::new(header, message, len, ResColor {
|
||||
r: 0,
|
||||
g: 0,
|
||||
b: 0,
|
||||
a: 255
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn color_notification(header: &'static str, message: &'static str, len: u32, color: ResColor) {
|
||||
unsafe {
|
||||
let queue = &mut QUEUE;
|
||||
queue.push(Notification::new(header, message, len, color));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn clear_notifications(header: &'static str) {
|
||||
unsafe {
|
||||
let queue = &mut QUEUE;
|
||||
queue.retain(|notif| notif.header != header);
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load diff
|
@ -1329,7 +1329,7 @@ impl<'a> SubMenu<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Content, Serialize)]
|
||||
#[derive(Content, Serialize, Clone)]
|
||||
pub struct Tab<'a> {
|
||||
pub tab_id: &'a str,
|
||||
pub tab_title: &'a str,
|
||||
|
@ -1372,12 +1372,12 @@ impl<'a> Tab<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Content, Serialize)]
|
||||
#[derive(Content, Serialize, Clone)]
|
||||
pub struct UiMenu<'a> {
|
||||
pub tabs: Vec<Tab<'a>>,
|
||||
}
|
||||
|
||||
pub unsafe fn get_menu() -> UiMenu<'static> {
|
||||
pub unsafe fn ui_menu(menu: TrainingModpackMenu) -> UiMenu<'static> {
|
||||
let mut overall_menu = UiMenu { tabs: Vec::new() };
|
||||
|
||||
let mut mash_tab = Tab {
|
||||
|
@ -1390,98 +1390,98 @@ pub unsafe fn get_menu() -> UiMenu<'static> {
|
|||
"mash_state",
|
||||
"Mash Toggles: Actions to be performed as soon as possible",
|
||||
false,
|
||||
&(MENU.mash_state.bits as u32),
|
||||
&(menu.mash_state.bits as u32),
|
||||
);
|
||||
mash_tab.add_submenu_with_toggles::<Action>(
|
||||
"Followup Toggles",
|
||||
"follow_up",
|
||||
"Followup Toggles: Actions to be performed after the Mash option",
|
||||
false,
|
||||
&(MENU.follow_up.bits as u32),
|
||||
&(menu.follow_up.bits as u32),
|
||||
);
|
||||
mash_tab.add_submenu_with_toggles::<MashTrigger>(
|
||||
"Mash Triggers",
|
||||
"mash_triggers",
|
||||
"Mash triggers: When the Mash Option will be performed",
|
||||
false,
|
||||
&(MENU.mash_triggers.bits as u32),
|
||||
&(menu.mash_triggers.bits as u32),
|
||||
);
|
||||
mash_tab.add_submenu_with_toggles::<AttackAngle>(
|
||||
"Attack Angle",
|
||||
"attack_angle",
|
||||
"Attack Angle: For attacks that can be angled, such as some forward tilts",
|
||||
false,
|
||||
&(MENU.attack_angle.bits as u32),
|
||||
&(menu.attack_angle.bits as u32),
|
||||
);
|
||||
mash_tab.add_submenu_with_toggles::<ThrowOption>(
|
||||
"Throw Options",
|
||||
"throw_state",
|
||||
"Throw Options: Throw to be performed when a grab is landed",
|
||||
false,
|
||||
&(MENU.throw_state.bits as u32),
|
||||
&(menu.throw_state.bits as u32),
|
||||
);
|
||||
mash_tab.add_submenu_with_toggles::<MedDelay>(
|
||||
"Throw Delay",
|
||||
"throw_delay",
|
||||
"Throw Delay: How many frames to delay the throw option",
|
||||
false,
|
||||
&(MENU.throw_delay.bits as u32),
|
||||
&(menu.throw_delay.bits as u32),
|
||||
);
|
||||
mash_tab.add_submenu_with_toggles::<MedDelay>(
|
||||
"Pummel Delay",
|
||||
"pummel_delay",
|
||||
"Pummel Delay: How many frames after a grab to wait before starting to pummel",
|
||||
false,
|
||||
&(MENU.pummel_delay.bits as u32),
|
||||
&(menu.pummel_delay.bits as u32),
|
||||
);
|
||||
mash_tab.add_submenu_with_toggles::<BoolFlag>(
|
||||
"Falling Aerials",
|
||||
"falling_aerials",
|
||||
"Falling Aerials: Should aerials be performed when rising or when falling",
|
||||
false,
|
||||
&(MENU.falling_aerials.bits as u32),
|
||||
&(menu.falling_aerials.bits as u32),
|
||||
);
|
||||
mash_tab.add_submenu_with_toggles::<BoolFlag>(
|
||||
"Full Hop",
|
||||
"full_hop",
|
||||
"Full Hop: Should the CPU perform a full hop or a short hop",
|
||||
false,
|
||||
&(MENU.full_hop.bits as u32),
|
||||
&(menu.full_hop.bits as u32),
|
||||
);
|
||||
mash_tab.add_submenu_with_toggles::<Delay>(
|
||||
"Aerial Delay",
|
||||
"aerial_delay",
|
||||
"Aerial Delay: How long to delay a Mash aerial attack",
|
||||
false,
|
||||
&(MENU.aerial_delay.bits as u32),
|
||||
&(menu.aerial_delay.bits as u32),
|
||||
);
|
||||
mash_tab.add_submenu_with_toggles::<BoolFlag>(
|
||||
"Fast Fall",
|
||||
"fast_fall",
|
||||
"Fast Fall: Should the CPU fastfall during a jump",
|
||||
false,
|
||||
&(MENU.fast_fall.bits as u32),
|
||||
&(menu.fast_fall.bits as u32),
|
||||
);
|
||||
mash_tab.add_submenu_with_toggles::<Delay>(
|
||||
"Fast Fall Delay",
|
||||
"fast_fall_delay",
|
||||
"Fast Fall Delay: How many frames the CPU should delay their fastfall",
|
||||
false,
|
||||
&(MENU.fast_fall_delay.bits as u32),
|
||||
&(menu.fast_fall_delay.bits as u32),
|
||||
);
|
||||
mash_tab.add_submenu_with_toggles::<Delay>(
|
||||
"OoS Offset",
|
||||
"oos_offset",
|
||||
"OoS Offset: How many times the CPU shield can be hit before performing a Mash option",
|
||||
false,
|
||||
&(MENU.oos_offset.bits as u32),
|
||||
&(menu.oos_offset.bits as u32),
|
||||
);
|
||||
mash_tab.add_submenu_with_toggles::<Delay>(
|
||||
"Reaction Time",
|
||||
"reaction_time",
|
||||
"Reaction Time: How many frames to delay before performing a mash option",
|
||||
false,
|
||||
&(MENU.reaction_time.bits as u32),
|
||||
&(menu.reaction_time.bits as u32),
|
||||
);
|
||||
overall_menu.tabs.push(mash_tab);
|
||||
|
||||
|
@ -1495,77 +1495,77 @@ pub unsafe fn get_menu() -> UiMenu<'static> {
|
|||
"air_dodge_dir",
|
||||
"Airdodge Direction: Direction to angle airdodges",
|
||||
false,
|
||||
&(MENU.air_dodge_dir.bits as u32),
|
||||
&(menu.air_dodge_dir.bits as u32),
|
||||
);
|
||||
defensive_tab.add_submenu_with_toggles::<Direction>(
|
||||
"DI Direction",
|
||||
"di_state",
|
||||
"DI Direction: Direction to angle the directional influence during hitlag",
|
||||
false,
|
||||
&(MENU.di_state.bits as u32),
|
||||
&(menu.di_state.bits as u32),
|
||||
);
|
||||
defensive_tab.add_submenu_with_toggles::<Direction>(
|
||||
"SDI Direction",
|
||||
"sdi_state",
|
||||
"SDI Direction: Direction to angle the smash directional influence during hitlag",
|
||||
false,
|
||||
&(MENU.sdi_state.bits as u32),
|
||||
&(menu.sdi_state.bits as u32),
|
||||
);
|
||||
defensive_tab.add_submenu_with_toggles::<SdiFrequency>(
|
||||
"SDI Strength",
|
||||
"sdi_strength",
|
||||
"SDI Strength: Relative strength of the smash directional influence inputs",
|
||||
true,
|
||||
&(MENU.sdi_strength as u32),
|
||||
&(menu.sdi_strength as u32),
|
||||
);
|
||||
defensive_tab.add_submenu_with_toggles::<ClatterFrequency>(
|
||||
"Clatter Strength",
|
||||
"clatter_strength",
|
||||
"Clatter Strength: Relative strength of the mashing out of grabs, buries, etc.",
|
||||
true,
|
||||
&(MENU.clatter_strength as u32),
|
||||
&(menu.clatter_strength as u32),
|
||||
);
|
||||
defensive_tab.add_submenu_with_toggles::<LedgeOption>(
|
||||
"Ledge Options",
|
||||
"ledge_state",
|
||||
"Ledge Options: Actions to be taken when on the ledge",
|
||||
false,
|
||||
&(MENU.ledge_state.bits as u32),
|
||||
&(menu.ledge_state.bits as u32),
|
||||
);
|
||||
defensive_tab.add_submenu_with_toggles::<LongDelay>(
|
||||
"Ledge Delay",
|
||||
"ledge_delay",
|
||||
"Ledge Delay: How many frames to delay the ledge option",
|
||||
false,
|
||||
&(MENU.ledge_delay.bits as u32),
|
||||
&(menu.ledge_delay.bits as u32),
|
||||
);
|
||||
defensive_tab.add_submenu_with_toggles::<TechFlags>(
|
||||
"Tech Options",
|
||||
"tech_state",
|
||||
"Tech Options: Actions to take when slammed into a hard surface",
|
||||
false,
|
||||
&(MENU.tech_state.bits as u32),
|
||||
&(menu.tech_state.bits as u32),
|
||||
);
|
||||
defensive_tab.add_submenu_with_toggles::<MissTechFlags>(
|
||||
"Mistech Options",
|
||||
"miss_tech_state",
|
||||
"Mistech Options: Actions to take after missing a tech",
|
||||
false,
|
||||
&(MENU.miss_tech_state.bits as u32),
|
||||
&(menu.miss_tech_state.bits as u32),
|
||||
);
|
||||
defensive_tab.add_submenu_with_toggles::<Shield>(
|
||||
"Shield Toggles",
|
||||
"shield_state",
|
||||
"Shield Toggles: CPU Shield Behavior",
|
||||
true,
|
||||
&(MENU.shield_state as u32),
|
||||
&(menu.shield_state as u32),
|
||||
);
|
||||
defensive_tab.add_submenu_with_toggles::<Direction>(
|
||||
"Shield Tilt",
|
||||
"shield_tilt",
|
||||
"Shield Tilt: Direction to tilt the shield",
|
||||
false, // TODO: Should this be true?
|
||||
&(MENU.shield_tilt.bits as u32),
|
||||
&(menu.shield_tilt.bits as u32),
|
||||
);
|
||||
|
||||
defensive_tab.add_submenu_with_toggles::<OnOff>(
|
||||
|
@ -1573,7 +1573,7 @@ pub unsafe fn get_menu() -> UiMenu<'static> {
|
|||
"crouch",
|
||||
"Crouch: Should the CPU crouch when on the ground",
|
||||
true,
|
||||
&(MENU.crouch as u32),
|
||||
&(menu.crouch as u32),
|
||||
);
|
||||
overall_menu.tabs.push(defensive_tab);
|
||||
|
||||
|
@ -1587,63 +1587,63 @@ pub unsafe fn get_menu() -> UiMenu<'static> {
|
|||
"save_state_mirroring",
|
||||
"Mirroring: Flips save states in the left-right direction across the stage center",
|
||||
true,
|
||||
&(MENU.save_state_mirroring as u32),
|
||||
&(menu.save_state_mirroring as u32),
|
||||
);
|
||||
save_state_tab.add_submenu_with_toggles::<OnOff>(
|
||||
"Auto Save States",
|
||||
"save_state_autoload",
|
||||
"Auto Save States: Load save state when any fighter dies",
|
||||
true,
|
||||
&(MENU.save_state_autoload as u32),
|
||||
&(menu.save_state_autoload as u32),
|
||||
);
|
||||
save_state_tab.add_submenu_with_toggles::<SaveDamage>(
|
||||
"Save Dmg (CPU)",
|
||||
"save_damage_cpu",
|
||||
"Save Damage: Should save states retain CPU damage",
|
||||
true,
|
||||
&(MENU.save_damage_cpu.bits as u32),
|
||||
&(menu.save_damage_cpu.bits as u32),
|
||||
);
|
||||
save_state_tab.add_submenu_with_slider::<DamagePercent>(
|
||||
"Dmg Range (CPU)",
|
||||
"save_damage_limits_cpu",
|
||||
"Limits on random damage to apply to the CPU when loading a save state",
|
||||
&(MENU.save_damage_limits_cpu.0 as u32),
|
||||
&(MENU.save_damage_limits_cpu.1 as u32),
|
||||
&(menu.save_damage_limits_cpu.0 as u32),
|
||||
&(menu.save_damage_limits_cpu.1 as u32),
|
||||
);
|
||||
save_state_tab.add_submenu_with_toggles::<SaveDamage>(
|
||||
"Save Dmg (Player)",
|
||||
"save_damage_player",
|
||||
"Save Damage: Should save states retain player damage",
|
||||
true,
|
||||
&(MENU.save_damage_player.bits as u32),
|
||||
&(menu.save_damage_player.bits as u32),
|
||||
);
|
||||
save_state_tab.add_submenu_with_slider::<DamagePercent>(
|
||||
"Dmg Range (Player)",
|
||||
"save_damage_limits_player",
|
||||
"Limits on random damage to apply to the player when loading a save state",
|
||||
&(MENU.save_damage_limits_player.0 as u32),
|
||||
&(MENU.save_damage_limits_player.1 as u32),
|
||||
&(menu.save_damage_limits_player.0 as u32),
|
||||
&(menu.save_damage_limits_player.1 as u32),
|
||||
);
|
||||
save_state_tab.add_submenu_with_toggles::<OnOff>(
|
||||
"Enable Save States",
|
||||
"save_state_enable",
|
||||
"Save States: Enable save states! Save a state with Grab+Down Taunt, load it with Grab+Up Taunt.",
|
||||
true,
|
||||
&(MENU.save_state_enable as u32),
|
||||
&(menu.save_state_enable as u32),
|
||||
);
|
||||
save_state_tab.add_submenu_with_toggles::<CharacterItem>(
|
||||
"Character Item",
|
||||
"character_item",
|
||||
"Character Item: CPU/Player item to hold when loading a save state",
|
||||
true,
|
||||
&(MENU.character_item as u32),
|
||||
&(menu.character_item as u32),
|
||||
);
|
||||
save_state_tab.add_submenu_with_toggles::<BuffOption>(
|
||||
"Buff Options",
|
||||
"buff_state",
|
||||
"Buff Options: Buff(s) to be applied to respective character when loading save states",
|
||||
false,
|
||||
&(MENU.buff_state.bits as u32),
|
||||
&(menu.buff_state.bits as u32),
|
||||
);
|
||||
overall_menu.tabs.push(save_state_tab);
|
||||
|
||||
|
@ -1657,42 +1657,42 @@ pub unsafe fn get_menu() -> UiMenu<'static> {
|
|||
"frame_advantage",
|
||||
"Frame Advantage: Display the time difference between when the player is actionable and the CPU is actionable",
|
||||
true,
|
||||
&(MENU.frame_advantage as u32),
|
||||
&(menu.frame_advantage as u32),
|
||||
);
|
||||
misc_tab.add_submenu_with_toggles::<OnOff>(
|
||||
"Hitbox Visualization",
|
||||
"hitbox_vis",
|
||||
"Hitbox Visualization: Should hitboxes be displayed, hiding other visual effects",
|
||||
true,
|
||||
&(MENU.hitbox_vis as u32),
|
||||
&(menu.hitbox_vis as u32),
|
||||
);
|
||||
misc_tab.add_submenu_with_toggles::<Delay>(
|
||||
"Input Delay",
|
||||
"input_delay",
|
||||
"Input Delay: Frames to delay player inputs by",
|
||||
true,
|
||||
&(MENU.input_delay.bits as u32),
|
||||
&(menu.input_delay.bits as u32),
|
||||
);
|
||||
misc_tab.add_submenu_with_toggles::<OnOff>(
|
||||
"Stage Hazards",
|
||||
"stage_hazards",
|
||||
"Stage Hazards: Should stage hazards be present",
|
||||
true,
|
||||
&(MENU.stage_hazards as u32),
|
||||
&(menu.stage_hazards as u32),
|
||||
);
|
||||
misc_tab.add_submenu_with_toggles::<OnOff>(
|
||||
"Quick Menu",
|
||||
"quick_menu",
|
||||
"Quick Menu: Should use quick or web menu",
|
||||
true,
|
||||
&(MENU.quick_menu as u32),
|
||||
&(menu.quick_menu as u32),
|
||||
);
|
||||
misc_tab.add_submenu_with_toggles::<OnOff>(
|
||||
"HUD",
|
||||
"hud",
|
||||
"HUD: Turn UI on or off",
|
||||
true,
|
||||
&(MENU.hud as u32),
|
||||
&(menu.hud as u32),
|
||||
);
|
||||
overall_menu.tabs.push(misc_tab);
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use training_mod_consts::{Slider, SubMenu, SubMenuType, Toggle, UiMenu};
|
||||
use training_mod_consts::{MenuJsonStruct, Slider, SubMenu, SubMenuType, Toggle, UiMenu, ui_menu, TrainingModpackMenu};
|
||||
use tui::{
|
||||
backend::Backend,
|
||||
layout::{Constraint, Corner, Direction, Layout, Rect},
|
||||
|
@ -20,6 +20,14 @@ use crate::list::{MultiStatefulList, StatefulList};
|
|||
|
||||
static NX_TUI_WIDTH: u16 = 66;
|
||||
|
||||
#[derive(PartialEq)]
|
||||
pub enum AppPage {
|
||||
SUBMENU,
|
||||
TOGGLE,
|
||||
SLIDER,
|
||||
CONFIRMATION
|
||||
}
|
||||
|
||||
/// We should hold a list of SubMenus.
|
||||
/// The currently selected SubMenu should also have an associated list with necessary information.
|
||||
/// We can convert the option types (Toggle, OnOff, Slider) to lists
|
||||
|
@ -28,11 +36,12 @@ pub struct App<'a> {
|
|||
pub menu_items: HashMap<&'a str, MultiStatefulList<SubMenu<'a>>>,
|
||||
pub selected_sub_menu_toggles: MultiStatefulList<Toggle<'a>>,
|
||||
pub selected_sub_menu_slider: DoubleEndedGauge,
|
||||
pub outer_list: bool,
|
||||
pub page: AppPage,
|
||||
pub default_menu: (UiMenu<'a>, String),
|
||||
}
|
||||
|
||||
impl<'a> App<'a> {
|
||||
pub fn new(menu: UiMenu<'a>) -> App<'a> {
|
||||
pub fn new(menu: UiMenu<'a>, default_menu: (UiMenu<'a>, String)) -> App<'a> {
|
||||
let num_lists = 3;
|
||||
|
||||
let mut menu_items_stateful = HashMap::new();
|
||||
|
@ -48,7 +57,8 @@ impl<'a> App<'a> {
|
|||
menu_items: menu_items_stateful,
|
||||
selected_sub_menu_toggles: MultiStatefulList::with_items(vec![], 0),
|
||||
selected_sub_menu_slider: DoubleEndedGauge::new(),
|
||||
outer_list: true,
|
||||
page: AppPage::SUBMENU,
|
||||
default_menu: default_menu
|
||||
};
|
||||
app.set_sub_menu_items();
|
||||
app
|
||||
|
@ -269,7 +279,7 @@ impl<'a> App<'a> {
|
|||
}
|
||||
|
||||
/// Different behavior depending on the current menu location
|
||||
/// Outer list: Sets self.outer_list to false
|
||||
/// Submenu list: Enters toggle or slider submenu
|
||||
/// Toggle submenu: Toggles the selected submenu toggle in self.selected_sub_menu_toggles and in the actual SubMenu struct
|
||||
/// Slider submenu: Swaps hover/selected state. Updates the actual SubMenu struct if going from Selected -> Hover
|
||||
pub fn on_a(&mut self) {
|
||||
|
@ -288,14 +298,14 @@ impl<'a> App<'a> {
|
|||
.items
|
||||
.get_mut(list_idx)
|
||||
.unwrap();
|
||||
if self.outer_list {
|
||||
self.outer_list = false;
|
||||
if self.page == AppPage::SUBMENU {
|
||||
match SubMenuType::from_str(selected_sub_menu._type) {
|
||||
// Need to change the slider state to MinHover so the slider shows up initially
|
||||
SubMenuType::SLIDER => {
|
||||
self.page = AppPage::SLIDER;
|
||||
self.selected_sub_menu_slider.state = GaugeState::MinHover;
|
||||
}
|
||||
_ => {}
|
||||
SubMenuType::TOGGLE => self.page = AppPage::TOGGLE
|
||||
}
|
||||
} else {
|
||||
match SubMenuType::from_str(selected_sub_menu._type) {
|
||||
|
@ -371,9 +381,9 @@ impl<'a> App<'a> {
|
|||
}
|
||||
|
||||
/// Different behavior depending on the current menu location
|
||||
/// Outer list: None
|
||||
/// Toggle submenu: Sets self.outer_list to true
|
||||
/// Slider submenu: If in a selected state, then commit changes and change to hover. Else set self.outer_list to true
|
||||
/// Submenu selection: None
|
||||
/// Toggle submenu: Sets page to submenu selection
|
||||
/// Slider submenu: If in a selected state, then commit changes and change to hover. Else set page to submenu selection
|
||||
pub fn on_b(&mut self) {
|
||||
let tab_selected = self
|
||||
.tabs
|
||||
|
@ -417,26 +427,55 @@ impl<'a> App<'a> {
|
|||
},
|
||||
_ => {}
|
||||
}
|
||||
self.outer_list = true;
|
||||
self.page = AppPage::SUBMENU;
|
||||
self.set_sub_menu_items();
|
||||
}
|
||||
|
||||
/// Save defaults command
|
||||
pub fn on_x(&mut self) {
|
||||
if self.page == AppPage::SUBMENU {
|
||||
let json = self.to_json();
|
||||
unsafe {
|
||||
self.default_menu = (ui_menu(serde_json::from_str::<TrainingModpackMenu>(&json).unwrap()), json);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Reset current submenu to defaults
|
||||
pub fn on_l(&mut self) {
|
||||
if self.outer_list {
|
||||
if self.page == AppPage::TOGGLE || self.page == AppPage::SLIDER {
|
||||
let json = self.to_json();
|
||||
let mut json_value = serde_json::from_str::<serde_json::Value>(&json).unwrap();
|
||||
let selected_sub_menu= self.sub_menu_selected();
|
||||
let id = selected_sub_menu.submenu_id;
|
||||
let default_json_value = serde_json::from_str::<serde_json::Value>(&self.default_menu.1).unwrap();
|
||||
*json_value.get_mut(id).unwrap() = default_json_value.get(id).unwrap().clone();
|
||||
let new_menu = serde_json::from_value::<TrainingModpackMenu>(json_value).unwrap();
|
||||
*self = App::new(unsafe { ui_menu(new_menu) }, self.default_menu.clone());
|
||||
}
|
||||
}
|
||||
|
||||
/// Reset all menus to defaults
|
||||
pub fn on_r(&mut self) {
|
||||
*self = App::new(self.default_menu.0.clone(), self.default_menu.clone());
|
||||
}
|
||||
|
||||
pub fn on_zl(&mut self) {
|
||||
if self.page == AppPage::SUBMENU {
|
||||
self.tabs.previous();
|
||||
self.set_sub_menu_items();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn on_r(&mut self) {
|
||||
if self.outer_list {
|
||||
pub fn on_zr(&mut self) {
|
||||
if self.page == AppPage::SUBMENU {
|
||||
self.tabs.next();
|
||||
self.set_sub_menu_items();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn on_up(&mut self) {
|
||||
if self.outer_list {
|
||||
if self.page == AppPage::SUBMENU {
|
||||
self.menu_items
|
||||
.get_mut(
|
||||
self.tabs
|
||||
|
@ -447,13 +486,13 @@ impl<'a> App<'a> {
|
|||
.unwrap()
|
||||
.previous();
|
||||
self.set_sub_menu_items();
|
||||
} else {
|
||||
} else if self.page == AppPage::TOGGLE || self.page == AppPage::SLIDER {
|
||||
self.sub_menu_previous();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn on_down(&mut self) {
|
||||
if self.outer_list {
|
||||
if self.page == AppPage::SUBMENU {
|
||||
self.menu_items
|
||||
.get_mut(
|
||||
self.tabs
|
||||
|
@ -464,13 +503,13 @@ impl<'a> App<'a> {
|
|||
.unwrap()
|
||||
.next();
|
||||
self.set_sub_menu_items();
|
||||
} else {
|
||||
} else if self.page == AppPage::TOGGLE || self.page == AppPage::SLIDER {
|
||||
self.sub_menu_next();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn on_left(&mut self) {
|
||||
if self.outer_list {
|
||||
if self.page == AppPage::SUBMENU {
|
||||
self.menu_items
|
||||
.get_mut(
|
||||
self.tabs
|
||||
|
@ -481,13 +520,13 @@ impl<'a> App<'a> {
|
|||
.unwrap()
|
||||
.previous_list();
|
||||
self.set_sub_menu_items();
|
||||
} else {
|
||||
} else if self.page == AppPage::TOGGLE || self.page == AppPage::SLIDER {
|
||||
self.sub_menu_previous_list();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn on_right(&mut self) {
|
||||
if self.outer_list {
|
||||
if self.page == AppPage::SUBMENU {
|
||||
self.menu_items
|
||||
.get_mut(
|
||||
self.tabs
|
||||
|
@ -498,13 +537,221 @@ impl<'a> App<'a> {
|
|||
.unwrap()
|
||||
.next_list();
|
||||
self.set_sub_menu_items();
|
||||
} else {
|
||||
} else if self.page == AppPage::TOGGLE || self.page == AppPage::SLIDER {
|
||||
self.sub_menu_next_list();
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns JSON representation of current menu settings
|
||||
pub fn to_json(&self) -> String {
|
||||
let mut settings = Map::new();
|
||||
for key in self.menu_items.keys() {
|
||||
for list in &self.menu_items.get(key).unwrap().lists {
|
||||
for sub_menu in &list.items {
|
||||
if !sub_menu.toggles.is_empty() {
|
||||
let val: u32 = sub_menu
|
||||
.toggles
|
||||
.iter()
|
||||
.filter(|t| t.checked)
|
||||
.map(|t| t.toggle_value)
|
||||
.sum();
|
||||
settings.insert(sub_menu.submenu_id.to_string(), json!(val));
|
||||
} else if sub_menu.slider.is_some() {
|
||||
let s: &Slider = sub_menu.slider.as_ref().unwrap();
|
||||
let val: Vec<u32> = vec![s.selected_min, s.selected_max];
|
||||
settings.insert(sub_menu.submenu_id.to_string(), json!(val));
|
||||
} else {
|
||||
panic!("Could not collect settings for {:?}", sub_menu.submenu_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
serde_json::to_string(&settings).unwrap()
|
||||
}
|
||||
|
||||
pub fn ui<B: Backend>(f: &mut Frame<B>, app: &mut App) -> String {
|
||||
|
||||
/// 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()
|
||||
}
|
||||
}
|
||||
|
||||
fn render_submenu_page<B: Backend>(f: &mut Frame<B>, app: &mut App, list_chunks: Vec<Rect>, help_chunk: Rect) {
|
||||
let tab_selected = app.tab_selected();
|
||||
let mut item_help = None;
|
||||
for (list_section, stateful_list) in app
|
||||
.menu_items
|
||||
.get(tab_selected)
|
||||
.unwrap()
|
||||
.lists
|
||||
.iter()
|
||||
.enumerate()
|
||||
{
|
||||
let items: Vec<ListItem> = stateful_list
|
||||
.items
|
||||
.iter()
|
||||
.map(|i| {
|
||||
let lines = vec![Spans::from(if stateful_list.state.selected().is_some() {
|
||||
i.submenu_title.to_owned()
|
||||
} else {
|
||||
" ".to_owned() + i.submenu_title
|
||||
})];
|
||||
ListItem::new(lines).style(Style::default().fg(Color::White))
|
||||
})
|
||||
.collect();
|
||||
|
||||
let list = List::new(items)
|
||||
.block(
|
||||
Block::default()
|
||||
.title(if list_section == 0 { "Options" } else { "" })
|
||||
.style(Style::default().fg(Color::LightRed)),
|
||||
)
|
||||
.highlight_style(
|
||||
Style::default()
|
||||
.fg(Color::Green)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
)
|
||||
.highlight_symbol(">> ");
|
||||
|
||||
let mut state = stateful_list.state.clone();
|
||||
if state.selected().is_some() {
|
||||
item_help = Some(stateful_list.items[state.selected().unwrap()].help_text);
|
||||
}
|
||||
|
||||
f.render_stateful_widget(list, list_chunks[list_section], &mut state);
|
||||
}
|
||||
|
||||
let help_paragraph = Paragraph::new(
|
||||
item_help.unwrap_or("").replace('\"', "")
|
||||
+ "\nA: Enter sub-menu | B: Exit menu | ZL/ZR: Next tab | X: Save Defaults",
|
||||
)
|
||||
.style(Style::default().fg(Color::Cyan));
|
||||
f.render_widget(help_paragraph, help_chunk);
|
||||
}
|
||||
|
||||
pub fn render_toggle_page<B: Backend>(f: &mut Frame<B>, app: &mut App, list_chunks: Vec<Rect>, help_chunk: Rect) {
|
||||
let (title, help_text, mut sub_menu_str_lists) = app.sub_menu_strs_and_states();
|
||||
for list_section in 0..sub_menu_str_lists.len() {
|
||||
let sub_menu_str = sub_menu_str_lists[list_section].0.clone();
|
||||
let sub_menu_state = &mut sub_menu_str_lists[list_section].1;
|
||||
let values_items: Vec<ListItem> = sub_menu_str
|
||||
.iter()
|
||||
.map(|s| {
|
||||
ListItem::new(vec![Spans::from(
|
||||
(if s.0 { "X " } else { " " }).to_owned() + s.1,
|
||||
)])
|
||||
})
|
||||
.collect();
|
||||
|
||||
let values_list = List::new(values_items)
|
||||
.block(Block::default().title(if list_section == 0 { title } else { "" }))
|
||||
.start_corner(Corner::TopLeft)
|
||||
.highlight_style(
|
||||
Style::default()
|
||||
.fg(Color::LightGreen)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
)
|
||||
.highlight_symbol(">> ");
|
||||
f.render_stateful_widget(values_list, list_chunks[list_section], sub_menu_state);
|
||||
}
|
||||
let help_paragraph = Paragraph::new(
|
||||
help_text.replace('\"', "") + "\nA: Select toggle | B: Exit submenu | X: Reset to defaults",
|
||||
)
|
||||
.style(Style::default().fg(Color::Cyan));
|
||||
f.render_widget(help_paragraph, help_chunk);
|
||||
}
|
||||
|
||||
|
||||
pub fn render_slider_page<B: Backend>(f: &mut Frame<B>, app: &mut App, vertical_chunk: Rect, help_chunk: Rect) {
|
||||
let (_title, help_text, gauge_vals) = app.sub_menu_strs_for_slider();
|
||||
let abs_min = gauge_vals.abs_min;
|
||||
let abs_max = gauge_vals.abs_max;
|
||||
let selected_min = gauge_vals.selected_min;
|
||||
let selected_max = gauge_vals.selected_max;
|
||||
let lbl_ratio = 0.95; // Needed so that the upper limit label is visible
|
||||
let constraints = [
|
||||
Constraint::Ratio((lbl_ratio * (selected_min-abs_min) as f32) as u32, abs_max-abs_min),
|
||||
Constraint::Ratio((lbl_ratio * (selected_max-selected_min) as f32) as u32, abs_max-abs_min),
|
||||
Constraint::Ratio((lbl_ratio * (abs_max-selected_max) as f32) as u32, abs_max-abs_min),
|
||||
Constraint::Min(3), // For upper limit label
|
||||
];
|
||||
let gauge_chunks = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints(constraints)
|
||||
.split(vertical_chunk);
|
||||
|
||||
let slider_lbls = [
|
||||
abs_min,
|
||||
selected_min,
|
||||
selected_max,
|
||||
abs_max,
|
||||
];
|
||||
for (idx, lbl) in slider_lbls.iter().enumerate() {
|
||||
let mut line_set = tui::symbols::line::NORMAL;
|
||||
line_set.horizontal = "-";
|
||||
let mut gauge = LineGauge::default()
|
||||
.ratio(1.0)
|
||||
.label(format!("{}", lbl))
|
||||
.style(Style::default().fg(Color::White))
|
||||
.line_set(line_set)
|
||||
.gauge_style(Style::default().fg(Color::White).bg(Color::Black));
|
||||
if idx == 1 {
|
||||
// Slider between selected_min and selected_max
|
||||
match gauge_vals.state {
|
||||
GaugeState::MinHover => {
|
||||
gauge = gauge.style(Style::default().fg(Color::Red))
|
||||
}
|
||||
GaugeState::MinSelected => {
|
||||
gauge = gauge.style(Style::default().fg(Color::Green))
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
gauge = gauge.gauge_style(Style::default().fg(Color::Yellow).bg(Color::Black));
|
||||
} else if idx == 2 {
|
||||
// Slider between selected_max and abs_max
|
||||
match gauge_vals.state {
|
||||
GaugeState::MaxHover => {
|
||||
gauge = gauge.style(Style::default().fg(Color::Red))
|
||||
}
|
||||
GaugeState::MaxSelected => {
|
||||
gauge = gauge.style(Style::default().fg(Color::Green))
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
} else if idx == 3 {
|
||||
// Slider for abs_max
|
||||
// We only want the label to show, so set the line character to " "
|
||||
let mut line_set = tui::symbols::line::NORMAL;
|
||||
line_set.horizontal = " ";
|
||||
gauge = gauge.line_set(line_set);
|
||||
|
||||
// For some reason, the selected_max slider displays on top
|
||||
// So we need to change the abs_max slider styling to match
|
||||
// If the selected_max is close enough to the abs_max
|
||||
if (selected_max as f32 / abs_max as f32) > 0.95 {
|
||||
gauge = gauge.style(match gauge_vals.state {
|
||||
GaugeState::MaxHover => Style::default().fg(Color::Red),
|
||||
GaugeState::MaxSelected => Style::default().fg(Color::Green),
|
||||
_ => Style::default(),
|
||||
})
|
||||
}
|
||||
}
|
||||
f.render_widget(gauge, gauge_chunks[idx]);
|
||||
}
|
||||
|
||||
let help_paragraph = Paragraph::new(
|
||||
help_text.replace('\"', "") + "\nA: Select toggle | B: Exit submenu | X: Reset to defaults",
|
||||
)
|
||||
.style(Style::default().fg(Color::Cyan));
|
||||
f.render_widget(help_paragraph, help_chunk);
|
||||
}
|
||||
|
||||
/// Run
|
||||
pub fn ui<B: Backend>(f: &mut Frame<B>, app: &mut App) {
|
||||
let app_tabs = &app.tabs;
|
||||
let tab_selected = app_tabs.state.selected().unwrap();
|
||||
let mut span_selected = Spans::default();
|
||||
|
@ -611,202 +858,10 @@ pub fn ui<B: Backend>(f: &mut Frame<B>, app: &mut App) -> String {
|
|||
|
||||
f.render_widget(tabs, vertical_chunks[0]);
|
||||
|
||||
if app.outer_list {
|
||||
let tab_selected = app.tab_selected();
|
||||
let mut item_help = None;
|
||||
for (list_section, stateful_list) in app
|
||||
.menu_items
|
||||
.get(tab_selected)
|
||||
.unwrap()
|
||||
.lists
|
||||
.iter()
|
||||
.enumerate()
|
||||
{
|
||||
let items: Vec<ListItem> = stateful_list
|
||||
.items
|
||||
.iter()
|
||||
.map(|i| {
|
||||
let lines = vec![Spans::from(if stateful_list.state.selected().is_some() {
|
||||
i.submenu_title.to_owned()
|
||||
} else {
|
||||
" ".to_owned() + i.submenu_title
|
||||
})];
|
||||
ListItem::new(lines).style(Style::default().fg(Color::White))
|
||||
})
|
||||
.collect();
|
||||
|
||||
let list = List::new(items)
|
||||
.block(
|
||||
Block::default()
|
||||
.title(if list_section == 0 { "Options" } else { "" })
|
||||
.style(Style::default().fg(Color::LightRed)),
|
||||
)
|
||||
.highlight_style(
|
||||
Style::default()
|
||||
.fg(Color::Green)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
)
|
||||
.highlight_symbol(">> ");
|
||||
|
||||
let mut state = stateful_list.state.clone();
|
||||
if state.selected().is_some() {
|
||||
item_help = Some(stateful_list.items[state.selected().unwrap()].help_text);
|
||||
}
|
||||
|
||||
f.render_stateful_widget(list, list_chunks[list_section], &mut state);
|
||||
}
|
||||
|
||||
// TODO: Add Save Defaults
|
||||
let help_paragraph = Paragraph::new(
|
||||
item_help.unwrap_or("").replace('\"', "")
|
||||
+ "\nA: Enter sub-menu | B: Exit menu | ZL/ZR: Next tab",
|
||||
)
|
||||
.style(Style::default().fg(Color::Cyan));
|
||||
f.render_widget(help_paragraph, vertical_chunks[2]);
|
||||
} else {
|
||||
if matches!(app.selected_sub_menu_slider.state, GaugeState::None) {
|
||||
let (title, help_text, mut sub_menu_str_lists) = app.sub_menu_strs_and_states();
|
||||
for list_section in 0..sub_menu_str_lists.len() {
|
||||
let sub_menu_str = sub_menu_str_lists[list_section].0.clone();
|
||||
let sub_menu_state = &mut sub_menu_str_lists[list_section].1;
|
||||
let values_items: Vec<ListItem> = sub_menu_str
|
||||
.iter()
|
||||
.map(|s| {
|
||||
ListItem::new(vec![Spans::from(
|
||||
(if s.0 { "X " } else { " " }).to_owned() + s.1,
|
||||
)])
|
||||
})
|
||||
.collect();
|
||||
|
||||
let values_list = List::new(values_items)
|
||||
.block(Block::default().title(if list_section == 0 { title } else { "" }))
|
||||
.start_corner(Corner::TopLeft)
|
||||
.highlight_style(
|
||||
Style::default()
|
||||
.fg(Color::LightGreen)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
)
|
||||
.highlight_symbol(">> ");
|
||||
f.render_stateful_widget(values_list, list_chunks[list_section], sub_menu_state);
|
||||
}
|
||||
let help_paragraph = Paragraph::new(
|
||||
help_text.replace('\"', "") + "\nA: Select toggle | B: Exit submenu",
|
||||
)
|
||||
.style(Style::default().fg(Color::Cyan));
|
||||
f.render_widget(help_paragraph, vertical_chunks[2]);
|
||||
} else {
|
||||
let (_title, help_text, gauge_vals) = app.sub_menu_strs_for_slider();
|
||||
let abs_min = gauge_vals.abs_min;
|
||||
let abs_max = gauge_vals.abs_max;
|
||||
let selected_min = gauge_vals.selected_min;
|
||||
let selected_max = gauge_vals.selected_max;
|
||||
let lbl_ratio = 0.95; // Needed so that the upper limit label is visible
|
||||
let constraints = [
|
||||
Constraint::Ratio((lbl_ratio * (selected_min-abs_min) as f32) as u32, abs_max-abs_min),
|
||||
Constraint::Ratio((lbl_ratio * (selected_max-selected_min) as f32) as u32, abs_max-abs_min),
|
||||
Constraint::Ratio((lbl_ratio * (abs_max-selected_max) as f32) as u32, abs_max-abs_min),
|
||||
Constraint::Min(3), // For upper limit label
|
||||
];
|
||||
let gauge_chunks = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints(constraints)
|
||||
.split(vertical_chunks[1]);
|
||||
|
||||
let slider_lbls = [
|
||||
abs_min,
|
||||
selected_min,
|
||||
selected_max,
|
||||
abs_max,
|
||||
];
|
||||
for (idx, lbl) in slider_lbls.iter().enumerate() {
|
||||
let mut line_set = tui::symbols::line::NORMAL;
|
||||
line_set.horizontal = "-";
|
||||
let mut gauge = LineGauge::default()
|
||||
.ratio(1.0)
|
||||
.label(format!("{}", lbl))
|
||||
.style(Style::default().fg(Color::White))
|
||||
.line_set(line_set)
|
||||
.gauge_style(Style::default().fg(Color::White).bg(Color::Black));
|
||||
if idx == 1 {
|
||||
// Slider between selected_min and selected_max
|
||||
match gauge_vals.state {
|
||||
GaugeState::MinHover => {
|
||||
gauge = gauge.style(Style::default().fg(Color::Red))
|
||||
}
|
||||
GaugeState::MinSelected => {
|
||||
gauge = gauge.style(Style::default().fg(Color::Green))
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
gauge = gauge.gauge_style(Style::default().fg(Color::Yellow).bg(Color::Black));
|
||||
} else if idx == 2 {
|
||||
// Slider between selected_max and abs_max
|
||||
match gauge_vals.state {
|
||||
GaugeState::MaxHover => {
|
||||
gauge = gauge.style(Style::default().fg(Color::Red))
|
||||
}
|
||||
GaugeState::MaxSelected => {
|
||||
gauge = gauge.style(Style::default().fg(Color::Green))
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
} else if idx == 3 {
|
||||
// Slider for abs_max
|
||||
// We only want the label to show, so set the line character to " "
|
||||
let mut line_set = tui::symbols::line::NORMAL;
|
||||
line_set.horizontal = " ";
|
||||
gauge = gauge.line_set(line_set);
|
||||
|
||||
// For some reason, the selected_max slider displays on top
|
||||
// So we need to change the abs_max slider styling to match
|
||||
// If the selected_max is close enough to the abs_max
|
||||
if (selected_max as f32 / abs_max as f32) > 0.95 {
|
||||
gauge = gauge.style(match gauge_vals.state {
|
||||
GaugeState::MaxHover => Style::default().fg(Color::Red),
|
||||
GaugeState::MaxSelected => Style::default().fg(Color::Green),
|
||||
_ => Style::default(),
|
||||
})
|
||||
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!()
|
||||
}
|
||||
}
|
||||
f.render_widget(gauge, gauge_chunks[idx]);
|
||||
}
|
||||
|
||||
let help_paragraph = Paragraph::new(
|
||||
help_text.replace('\"', "") + "\nA: Select toggle | B: Exit submenu",
|
||||
)
|
||||
.style(Style::default().fg(Color::Cyan));
|
||||
f.render_widget(help_paragraph, vertical_chunks[2]);
|
||||
}
|
||||
}
|
||||
|
||||
// Collect settings
|
||||
to_json(app)
|
||||
|
||||
// TODO: Add saveDefaults
|
||||
}
|
||||
|
||||
pub fn to_json(app: &App) -> String {
|
||||
let mut settings = Map::new();
|
||||
for key in app.menu_items.keys() {
|
||||
for list in &app.menu_items.get(key).unwrap().lists {
|
||||
for sub_menu in &list.items {
|
||||
if !sub_menu.toggles.is_empty() {
|
||||
let val: u32 = sub_menu
|
||||
.toggles
|
||||
.iter()
|
||||
.filter(|t| t.checked)
|
||||
.map(|t| t.toggle_value)
|
||||
.sum();
|
||||
settings.insert(sub_menu.submenu_id.to_string(), json!(val));
|
||||
} else if sub_menu.slider.is_some() {
|
||||
let s: &Slider = sub_menu.slider.as_ref().unwrap();
|
||||
let val: Vec<u32> = vec![s.selected_min, s.selected_max];
|
||||
settings.insert(sub_menu.submenu_id.to_string(), json!(val));
|
||||
} else {
|
||||
panic!("Could not collect settings for {:?}", sub_menu.submenu_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
serde_json::to_string(&settings).unwrap()
|
||||
}
|
||||
|
|
|
@ -17,10 +17,10 @@ use tui::Terminal;
|
|||
|
||||
use training_mod_consts::*;
|
||||
|
||||
fn test_backend_setup(ui_menu: UiMenu) -> Result<
|
||||
(Terminal<training_mod_tui::TestBackend>, training_mod_tui::App),
|
||||
fn test_backend_setup<'a>(ui_menu: UiMenu<'a>, menu_defaults: (UiMenu<'a>, String)) -> Result<
|
||||
(Terminal<training_mod_tui::TestBackend>, training_mod_tui::App<'a>),
|
||||
Box<dyn Error>> {
|
||||
let app = training_mod_tui::App::new(ui_menu);
|
||||
let app = training_mod_tui::App::<'a>::new(ui_menu, menu_defaults);
|
||||
let backend = tui::backend::TestBackend::new(75, 15);
|
||||
let terminal = Terminal::new(backend)?;
|
||||
let mut state = tui::widgets::ListState::default();
|
||||
|
@ -30,26 +30,154 @@ fn test_backend_setup(ui_menu: UiMenu) -> Result<
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn ensure_menu_retains_selections() -> Result<(), Box<dyn Error>> {
|
||||
fn test_set_airdodge() -> Result<(), Box<dyn Error>> {
|
||||
let menu;
|
||||
let prev_menu;
|
||||
let mut prev_menu;
|
||||
let menu_defaults;
|
||||
unsafe {
|
||||
prev_menu = MENU;
|
||||
menu = get_menu();
|
||||
prev_menu = MENU.clone();
|
||||
menu = ui_menu(MENU);
|
||||
menu_defaults = (ui_menu(MENU), serde_json::to_string(&MENU).unwrap());
|
||||
}
|
||||
|
||||
let (mut terminal, mut app) = test_backend_setup(menu)?;
|
||||
let mut json_response = String::new();
|
||||
let _frame_res = terminal.draw(|f| json_response = training_mod_tui::ui(f, &mut app))?;
|
||||
let (_terminal, mut app) = test_backend_setup(menu, menu_defaults)?;
|
||||
// Enter Mash Toggles
|
||||
app.on_a();
|
||||
// Set Mash Airdodge
|
||||
app.on_a();
|
||||
let menu_json = app.get_menu_selections();
|
||||
let menu_struct = serde_json::from_str::<MenuJsonStruct>(&menu_json).unwrap();
|
||||
let menu = menu_struct.menu;
|
||||
let _ = menu_struct.defaults_menu;
|
||||
prev_menu.mash_state.toggle(Action::AIR_DODGE);
|
||||
assert_eq!(
|
||||
serde_json::to_string(&prev_menu).unwrap(),
|
||||
serde_json::to_string(&menu).unwrap()
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ensure_menu_retains_selections() -> Result<(), Box<dyn Error>> {
|
||||
let menu;
|
||||
let prev_menu;
|
||||
let menu_defaults;
|
||||
unsafe {
|
||||
MENU = serde_json::from_str::<TrainingModpackMenu>(&json_response).unwrap();
|
||||
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()
|
||||
serde_json::to_string(&menu).unwrap()
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_save_and_reset_defaults() -> Result<(), Box<dyn Error>> {
|
||||
let menu;
|
||||
let mut prev_menu;
|
||||
let menu_defaults;
|
||||
unsafe {
|
||||
prev_menu = MENU.clone();
|
||||
menu = ui_menu(MENU);
|
||||
menu_defaults = (ui_menu(MENU), serde_json::to_string(&MENU).unwrap());
|
||||
}
|
||||
|
||||
let (_terminal, mut app) = test_backend_setup(menu, menu_defaults)?;
|
||||
|
||||
// Enter Mash Toggles
|
||||
app.on_a();
|
||||
// Set Mash Airdodge
|
||||
app.on_a();
|
||||
// Return to submenu selection
|
||||
app.on_b();
|
||||
// Save Defaults
|
||||
app.on_x();
|
||||
// Enter Mash Toggles again
|
||||
app.on_a();
|
||||
// Unset Mash Airdodge
|
||||
app.on_a();
|
||||
|
||||
let menu_json = app.get_menu_selections();
|
||||
let menu_struct = serde_json::from_str::<MenuJsonStruct>(&menu_json).unwrap();
|
||||
let menu = menu_struct.menu;
|
||||
let defaults_menu = menu_struct.defaults_menu;
|
||||
|
||||
assert_eq!(
|
||||
serde_json::to_string(&prev_menu).unwrap(),
|
||||
serde_json::to_string(&menu).unwrap(),
|
||||
"The menu should be the same as how we started"
|
||||
);
|
||||
prev_menu.mash_state.toggle(Action::AIR_DODGE);
|
||||
assert_eq!(
|
||||
serde_json::to_string(&prev_menu).unwrap(),
|
||||
serde_json::to_string(&defaults_menu).unwrap(),
|
||||
"The defaults menu should have Mash Airdodge"
|
||||
);
|
||||
|
||||
// Reset current menu alone to defaults
|
||||
app.on_l();
|
||||
let menu_json = app.get_menu_selections();
|
||||
let menu_struct = serde_json::from_str::<MenuJsonStruct>(&menu_json).unwrap();
|
||||
let menu = menu_struct.menu;
|
||||
let _ = menu_struct.defaults_menu;
|
||||
|
||||
assert_eq!(
|
||||
serde_json::to_string(&prev_menu).unwrap(),
|
||||
serde_json::to_string(&menu).unwrap(),
|
||||
"The menu should have Mash Airdodge"
|
||||
);
|
||||
|
||||
// Enter Mash Toggles
|
||||
app.on_a();
|
||||
// Unset Mash Airdodge
|
||||
app.on_a();
|
||||
// Return to submenu selection
|
||||
app.on_b();
|
||||
// Go down to Followup Toggles
|
||||
app.on_down();
|
||||
// Enter Followup Toggles
|
||||
app.on_a();
|
||||
// Go down and set Jump
|
||||
app.on_down();
|
||||
app.on_a();
|
||||
// Return to submenu selection
|
||||
app.on_b();
|
||||
// Save defaults
|
||||
app.on_x();
|
||||
// Go back in and unset Jump
|
||||
app.on_a();
|
||||
app.on_down();
|
||||
app.on_a();
|
||||
// Return to submenu selection
|
||||
app.on_b();
|
||||
// Reset all to defaults
|
||||
app.on_r();
|
||||
let menu_json = app.get_menu_selections();
|
||||
let menu_struct = serde_json::from_str::<MenuJsonStruct>(&menu_json).unwrap();
|
||||
let menu = menu_struct.menu;
|
||||
let _ = menu_struct.defaults_menu;
|
||||
|
||||
prev_menu.mash_state.toggle(Action::AIR_DODGE);
|
||||
prev_menu.follow_up.toggle(Action::JUMP);
|
||||
assert_eq!(
|
||||
serde_json::to_string(&prev_menu).unwrap(),
|
||||
serde_json::to_string(&menu).unwrap(),
|
||||
"The menu should have Mash Airdodge off and Followup Jump on"
|
||||
);
|
||||
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
@ -57,17 +185,23 @@ fn main() -> Result<(), Box<dyn Error>> {
|
|||
let args: Vec<String> = std::env::args().collect();
|
||||
let inputs = args.get(1);
|
||||
let menu;
|
||||
let menu_defaults;
|
||||
|
||||
unsafe {
|
||||
menu = get_menu();
|
||||
menu = ui_menu(MENU);
|
||||
menu_defaults = (ui_menu(MENU), serde_json::to_string(&MENU).unwrap());
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "has_terminal"))] {
|
||||
let (mut terminal, mut app) = test_backend_setup(menu)?;
|
||||
let (mut terminal, mut app) = test_backend_setup(menu, menu_defaults)?;
|
||||
if inputs.is_some() {
|
||||
inputs.unwrap().split(",").for_each(|input| {
|
||||
match input.to_uppercase().as_str() {
|
||||
"X" => app.on_x(),
|
||||
"L" => app.on_l(),
|
||||
"R" => app.on_r(),
|
||||
"O" => app.on_zl(),
|
||||
"P" => app.on_zr(),
|
||||
"A" => app.on_a(),
|
||||
"B" => app.on_b(),
|
||||
"UP" => app.on_up(),
|
||||
|
@ -78,8 +212,8 @@ fn main() -> Result<(), Box<dyn Error>> {
|
|||
}
|
||||
})
|
||||
}
|
||||
let mut json_response = String::new();
|
||||
let frame_res = terminal.draw(|f| json_response = training_mod_tui::ui(f, &mut app))?;
|
||||
let frame_res = terminal.draw(|f| training_mod_tui::ui(f, &mut app))?;
|
||||
let menu_json = app.get_menu_selections();
|
||||
|
||||
for (i, cell) in frame_res.buffer.content().iter().enumerate() {
|
||||
print!("{}", cell.symbol);
|
||||
|
@ -89,11 +223,11 @@ fn main() -> Result<(), Box<dyn Error>> {
|
|||
}
|
||||
println!();
|
||||
|
||||
println!("json_response:\n{}", json_response);
|
||||
println!("Menu:\n{menu_json}");
|
||||
}
|
||||
|
||||
#[cfg(feature = "has_terminal")] {
|
||||
let app = training_mod_tui::App::new(menu);
|
||||
let app = training_mod_tui::App::new(menu, menu_defaults);
|
||||
|
||||
// setup terminal
|
||||
enable_raw_mode()?;
|
||||
|
@ -117,10 +251,10 @@ fn main() -> Result<(), Box<dyn Error>> {
|
|||
if let Err(err) = res {
|
||||
println!("{:?}", err)
|
||||
} else {
|
||||
println!("JSON: {}", res.as_ref().unwrap());
|
||||
println!("JSONs: {:#?}", res.as_ref().unwrap());
|
||||
unsafe {
|
||||
MENU = serde_json::from_str::<TrainingModpackMenu>(&res.as_ref().unwrap()).unwrap();
|
||||
println!("MENU: {:#?}", MENU);
|
||||
let menu = serde_json::from_str::<MenuJsonStruct>(&res.as_ref().unwrap()).unwrap();
|
||||
println!("menu: {:#?}", menu);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -135,9 +269,9 @@ fn run_app<B: tui::backend::Backend>(
|
|||
tick_rate: Duration,
|
||||
) -> io::Result<String> {
|
||||
let mut last_tick = Instant::now();
|
||||
let mut json_response = String::new();
|
||||
loop {
|
||||
terminal.draw(|f| json_response = training_mod_tui::ui(f, &mut app).clone())?;
|
||||
terminal.draw(|f| training_mod_tui::ui(f, &mut app).clone())?;
|
||||
let menu_json = app.get_menu_selections();
|
||||
|
||||
let timeout = tick_rate
|
||||
.checked_sub(last_tick.elapsed())
|
||||
|
@ -146,7 +280,10 @@ fn run_app<B: tui::backend::Backend>(
|
|||
if crossterm::event::poll(timeout)? {
|
||||
if let Event::Key(key) = event::read()? {
|
||||
match key.code {
|
||||
KeyCode::Char('q') => return Ok(json_response),
|
||||
KeyCode::Char('q') => return Ok(menu_json),
|
||||
KeyCode::Char('x') => app.on_x(),
|
||||
KeyCode::Char('p') => app.on_zr(),
|
||||
KeyCode::Char('o') => app.on_zl(),
|
||||
KeyCode::Char('r') => app.on_r(),
|
||||
KeyCode::Char('l') => app.on_l(),
|
||||
KeyCode::Left => app.on_left(),
|
||||
|
|
Loading…
Reference in a new issue