1
0
Fork 0
mirror of https://github.com/jugeeya/UltimateTrainingModpack.git synced 2024-11-30 22:00:16 +00:00

Randomized percent on load (#394)

* Create save states tab

* Initial work - crashes on boot

* Change usizes to u32's.
Refactor toggle "checked" logic.
Add blujay's panic tracker.

* Rename tui mins/maxes

* Fix misc. TUI bugs

* Fix panic caused by prematurely setting the submenu state to GaugeState::None

Set submenu state to GaugeState::MinHover when opening a slider menu so that the slider is immediately loaded

When changing from GaugeState::Min/MaxSelected to Min/MaxHover, commit changes from App.current_sub_menu_slider to SubMenu.slider so that it can be exported to JSON

* Merge save_damage and save_state_pct_rand_enable settings

* Add comments to training_mod_tui::lib.rs

* Add icon

* Initial work on web slider

Todo:
Styling polish
Bugfix for initial settings load
Handle dragging using gamepad

* Style and fix web slider

* Add separate settings for player random damage

* TUI styling fixes

* Paginate TUI tabs

* Address CR comments
This commit is contained in:
asimon-1 2022-11-01 13:52:38 -07:00 committed by GitHub
parent aea5011a89
commit fa451986e0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 3063 additions and 2004 deletions

View file

@ -96,7 +96,7 @@ pub unsafe fn set_menu_from_json(message: &str) {
skyline::error::show_error( skyline::error::show_error(
0x70, 0x70,
"Could not parse the menu response!\nPlease send a screenshot of the details page to the developers.\n\0", "Could not parse the menu response!\nPlease send a screenshot of the details page to the developers.\n\0",
&*format!("{:#?}\0", web_response.err().unwrap()) &*format!("{:#?}\0", message)
); );
}; };
if MENU.quick_menu == OnOff::Off { if MENU.quick_menu == OnOff::Off {

View file

@ -54,6 +54,27 @@ macro_rules! c_str {
#[skyline::main(name = "training_modpack")] #[skyline::main(name = "training_modpack")]
pub fn main() { pub fn main() {
std::panic::set_hook(Box::new(|info| {
let location = info.location().unwrap();
let msg = match info.payload().downcast_ref::<&'static str>() {
Some(s) => *s,
None => {
match info.payload().downcast_ref::<String>() {
Some(s) => &s[..],
None => "Box<Any>",
}
},
};
let err_msg = format!("thread has panicked at '{}', {}", msg, location);
skyline::error::show_error(
69,
"Skyline plugin has panicked! Please open the details and send a screenshot to the developer, then close the game.\n",
err_msg.as_str(),
);
}));
macro_rules! log { macro_rules! log {
($($arg:tt)*) => { ($($arg:tt)*) => {
println!("{}{}", "[Training Modpack] ".green(), format!($($arg)*)); println!("{}{}", "[Training Modpack] ".green(), format!($($arg)*));

1
src/static/css/nouislider.min.css vendored Normal file
View file

@ -0,0 +1 @@
.noUi-target,.noUi-target *{-webkit-touch-callout:none;-webkit-tap-highlight-color:transparent;-webkit-user-select:none;-ms-touch-action:none;touch-action:none;-ms-user-select:none;-moz-user-select:none;user-select:none;-moz-box-sizing:border-box;box-sizing:border-box}.noUi-target{position:relative}.noUi-base,.noUi-connects{width:100%;height:100%;position:relative;z-index:1}.noUi-connects{overflow:hidden;z-index:0}.noUi-connect,.noUi-origin{will-change:transform;position:absolute;z-index:1;top:0;right:0;height:100%;width:100%;-ms-transform-origin:0 0;-webkit-transform-origin:0 0;-webkit-transform-style:preserve-3d;transform-origin:0 0;transform-style:flat}.noUi-txt-dir-rtl.noUi-horizontal .noUi-origin{left:0;right:auto}.noUi-vertical .noUi-origin{top:-100%;width:0}.noUi-horizontal .noUi-origin{height:0}.noUi-handle{-webkit-backface-visibility:hidden;backface-visibility:hidden;position:absolute}.noUi-touch-area{height:100%;width:100%}.noUi-state-tap .noUi-connect,.noUi-state-tap .noUi-origin{-webkit-transition:transform .3s;transition:transform .3s}.noUi-state-drag *{cursor:inherit!important}.noUi-horizontal{height:18px}.noUi-horizontal .noUi-handle{width:34px;height:28px;right:-17px;top:-6px}.noUi-vertical{width:18px}.noUi-vertical .noUi-handle{width:28px;height:34px;right:-6px;bottom:-17px}.noUi-txt-dir-rtl.noUi-horizontal .noUi-handle{left:-17px;right:auto}.noUi-target{background:#FAFAFA;border-radius:4px;border:1px solid #D3D3D3;box-shadow:inset 0 1px 1px #F0F0F0,0 3px 6px -5px #BBB}.noUi-connects{border-radius:3px}.noUi-connect{background:#3FB8AF}.noUi-draggable{cursor:ew-resize}.noUi-vertical .noUi-draggable{cursor:ns-resize}.noUi-handle{border:1px solid #D9D9D9;border-radius:3px;background:#FFF;cursor:default;box-shadow:inset 0 0 1px #FFF,inset 0 1px 7px #EBEBEB,0 3px 6px -3px #BBB}.noUi-active{box-shadow:inset 0 0 1px #FFF,inset 0 1px 7px #DDD,0 3px 6px -3px #BBB}.noUi-handle:after,.noUi-handle:before{content:"";display:block;position:absolute;height:14px;width:1px;background:#E8E7E6;left:14px;top:6px}.noUi-handle:after{left:17px}.noUi-vertical .noUi-handle:after,.noUi-vertical .noUi-handle:before{width:14px;height:1px;left:6px;top:14px}.noUi-vertical .noUi-handle:after{top:17px}[disabled] .noUi-connect{background:#B8B8B8}[disabled] .noUi-handle,[disabled].noUi-handle,[disabled].noUi-target{cursor:not-allowed}.noUi-pips,.noUi-pips *{-moz-box-sizing:border-box;box-sizing:border-box}.noUi-pips{position:absolute;color:#999}.noUi-value{position:absolute;white-space:nowrap;text-align:center}.noUi-value-sub{color:#ccc;font-size:10px}.noUi-marker{position:absolute;background:#CCC}.noUi-marker-sub{background:#AAA}.noUi-marker-large{background:#AAA}.noUi-pips-horizontal{padding:10px 0;height:80px;top:100%;left:0;width:100%}.noUi-value-horizontal{-webkit-transform:translate(-50%,50%);transform:translate(-50%,50%)}.noUi-rtl .noUi-value-horizontal{-webkit-transform:translate(50%,50%);transform:translate(50%,50%)}.noUi-marker-horizontal.noUi-marker{margin-left:-1px;width:2px;height:5px}.noUi-marker-horizontal.noUi-marker-sub{height:10px}.noUi-marker-horizontal.noUi-marker-large{height:15px}.noUi-pips-vertical{padding:0 10px;height:100%;top:0;left:100%}.noUi-value-vertical{-webkit-transform:translate(0,-50%);transform:translate(0,-50%);padding-left:25px}.noUi-rtl .noUi-value-vertical{-webkit-transform:translate(0,50%);transform:translate(0,50%)}.noUi-marker-vertical.noUi-marker{width:5px;height:2px;margin-top:-1px}.noUi-marker-vertical.noUi-marker-sub{width:10px}.noUi-marker-vertical.noUi-marker-large{width:15px}.noUi-tooltip{display:block;position:absolute;border:1px solid #D9D9D9;border-radius:3px;background:#fff;color:#000;padding:5px;text-align:center;white-space:nowrap}.noUi-horizontal .noUi-tooltip{-webkit-transform:translate(-50%,0);transform:translate(-50%,0);left:50%;bottom:120%}.noUi-vertical .noUi-tooltip{-webkit-transform:translate(0,-50%);transform:translate(0,-50%);top:50%;right:120%}.noUi-horizontal .noUi-origin>.noUi-tooltip{-webkit-transform:translate(50%,0);transform:translate(50%,0);left:auto;bottom:10px}.noUi-vertical .noUi-origin>.noUi-tooltip{-webkit-transform:translate(0,-18px);transform:translate(0,-18px);top:auto;right:28px}

View file

@ -185,7 +185,10 @@ body {
visibility: hidden; visibility: hidden;
} }
:focus { :focus:not(.noUi-handle),
.handleSelected,
.noUi-connect
{
background: rgb(255, 70, 2); background: rgb(255, 70, 2);
background: linear-gradient( background: linear-gradient(
45deg, 45deg,
@ -302,3 +305,38 @@ body {
text-align: center; text-align: center;
font-size: large; font-size: large;
} }
.modal-slider {
width: 70%;
margin-bottom: 170px;
}
.modal-slider-label {
width: 100%;
display: flex;
justify-content: center;
}
.modal-slider-label > p {
font-size: 26px;
padding: 0.5rem;
background: #ccc;
border-color: black;
border-radius: 0.5rem;
border-style: solid;
border-width: 0.25rem;
}
.noUi-value, .noUi-pips {
margin-top: 12px;
color: #000;
}
.noUi-marker, .noUi-marker-large {
background: #000;
}
.noUi-tooltip {
padding: 12px;
font-size: 1.5rem;
}

View file

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

View file

@ -0,0 +1,83 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="80.0px"
height="80.0px"
viewBox="0 0 80.0 80.0"
version="1.1"
id="SVGRoot"
sodipodi:docname="save_damage_limits.svg"
inkscape:version="1.1 (c68e22c387, 2021-05-23)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<defs
id="defs5493" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:zoom="7.9195959"
inkscape:cx="43.373425"
inkscape:cy="38.322662"
inkscape:document-units="px"
inkscape:current-layer="layer1"
inkscape:document-rotation="0"
showgrid="true"
inkscape:window-width="1904"
inkscape:window-height="1001"
inkscape:window-x="192"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:pagecheckerboard="1">
<inkscape:grid
type="xygrid"
id="grid6063" />
</sodipodi:namedview>
<metadata
id="metadata5496">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1">
<path
style="fill:#000000;stroke:#000000;stroke-width:6;stroke-linecap:round;stroke-linejoin:miter;stroke-opacity:1;stroke-miterlimit:4;stroke-dasharray:none"
d="M 15,65 65,15"
id="path6070" />
<circle
style="opacity:1;fill:none;stroke:#000000;stroke-width:6;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;paint-order:fill markers stroke"
id="path6072"
cx="22.5"
cy="22.5"
r="7.5" />
<circle
style="opacity:1;fill:none;stroke:#000000;stroke-width:6;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;paint-order:fill markers stroke"
id="path6072-8"
cx="60.051399"
cy="55.43021"
r="7.5" />
<path
style="fill:none;stroke:#000000;stroke-width:3;stroke-linecap:round;stroke-linejoin:miter;stroke-opacity:1;stroke-miterlimit:4;stroke-dasharray:none"
d="M 10,5 H 70"
id="path993" />
<path
style="fill:none;stroke:#000000;stroke-width:3;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 10,75 H 70"
id="path993-8" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

View file

@ -0,0 +1,83 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="80.0px"
height="80.0px"
viewBox="0 0 80.0 80.0"
version="1.1"
id="SVGRoot"
sodipodi:docname="save_damage_limits.svg"
inkscape:version="1.1 (c68e22c387, 2021-05-23)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<defs
id="defs5493" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:zoom="7.9195959"
inkscape:cx="43.373425"
inkscape:cy="38.322662"
inkscape:document-units="px"
inkscape:current-layer="layer1"
inkscape:document-rotation="0"
showgrid="true"
inkscape:window-width="1904"
inkscape:window-height="1001"
inkscape:window-x="192"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:pagecheckerboard="1">
<inkscape:grid
type="xygrid"
id="grid6063" />
</sodipodi:namedview>
<metadata
id="metadata5496">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1">
<path
style="fill:#000000;stroke:#000000;stroke-width:6;stroke-linecap:round;stroke-linejoin:miter;stroke-opacity:1;stroke-miterlimit:4;stroke-dasharray:none"
d="M 15,65 65,15"
id="path6070" />
<circle
style="opacity:1;fill:none;stroke:#000000;stroke-width:6;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;paint-order:fill markers stroke"
id="path6072"
cx="22.5"
cy="22.5"
r="7.5" />
<circle
style="opacity:1;fill:none;stroke:#000000;stroke-width:6;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;paint-order:fill markers stroke"
id="path6072-8"
cx="60.051399"
cy="55.43021"
r="7.5" />
<path
style="fill:none;stroke:#000000;stroke-width:3;stroke-linecap:round;stroke-linejoin:miter;stroke-opacity:1;stroke-miterlimit:4;stroke-dasharray:none"
d="M 10,5 H 70"
id="path993" />
<path
style="fill:none;stroke:#000000;stroke-width:3;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 10,75 H 70"
id="path993-8" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

View file

@ -0,0 +1,75 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="80.0px"
height="80.0px"
viewBox="0 0 80.0 80.0"
version="1.1"
id="SVGRoot"
sodipodi:docname="save_damage.svg"
inkscape:version="1.0.1 (3bc2e813f5, 2020-09-07)">
<defs
id="defs5493" />
<sodipodi:namedview
id="base"
pagecolor="#000000"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:zoom="7.9195959"
inkscape:cx="43.461799"
inkscape:cy="28.245216"
inkscape:document-units="px"
inkscape:current-layer="layer1"
inkscape:document-rotation="0"
showgrid="true"
inkscape:window-width="1431"
inkscape:window-height="1150"
inkscape:window-x="192"
inkscape:window-y="0"
inkscape:window-maximized="0">
<inkscape:grid
type="xygrid"
id="grid6063" />
</sodipodi:namedview>
<metadata
id="metadata5496">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1">
<path
style="fill:#000000;stroke:#000000;stroke-width:6;stroke-linecap:round;stroke-linejoin:miter;stroke-opacity:1;stroke-miterlimit:4;stroke-dasharray:none"
d="M 15,65 65,15"
id="path6070" />
<circle
style="opacity:1;fill:none;stroke:#000000;stroke-width:6;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;paint-order:fill markers stroke"
id="path6072"
cx="22.5"
cy="22.5"
r="7.5" />
<circle
style="opacity:1;fill:none;stroke:#000000;stroke-width:6;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;paint-order:fill markers stroke"
id="path6072-8"
cx="60.051399"
cy="55.43021"
r="7.5" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

1
src/static/js/nouislider.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View file

@ -23,17 +23,31 @@ var DEFAULTS_PREFIX = '__';
// Set input handlers // Set input handlers
if (isNx) { if (isNx) {
window.nx.footer.setAssign('A', '', function () { select(document.activeElement) }, { se: '' });
window.nx.footer.setAssign('B', '', closeOrExit, { se: '' }); window.nx.footer.setAssign('B', '', closeOrExit, { se: '' });
window.nx.footer.setAssign('X', '', resetCurrentMenu, { se: '' }); window.nx.footer.setAssign('X', '', resetCurrentMenu, { se: '' });
window.nx.footer.setAssign('L', '', resetAllMenus, { se: '' }); window.nx.footer.setAssign('L', '', resetAllMenus, { se: '' });
window.nx.footer.setAssign('R', '', saveDefaults, { se: '' }); window.nx.footer.setAssign('R', '', saveDefaults, { se: '' });
window.nx.footer.setAssign('ZR', '', cycleNextTab, { se: '' }); window.nx.footer.setAssign('ZR', '', cycleNextTab, { se: '' });
window.nx.footer.setAssign('ZL', '', cyclePrevTab, { se: '' }); window.nx.footer.setAssign('ZL', '', cyclePrevTab, { se: '' });
window.nx.addEventListener("message", function(msg) { setSettingsFromJSON(msg)}); window.nx.addEventListener("message", function (msg) { setSettingsFromJSON(msg.data) });
window.nx.sendMessage("loaded"); document.addEventListener('keydown', (event) => {
switch (event.keyCode) {
case 37: // Control stick left
decreaseSelectedHandle();
break;
case 39: // Control stick right
increaseSelectedHandle();
break;
}
})
} else { } else {
document.addEventListener('keypress', (event) => { document.addEventListener('keydown', (event) => {
switch (event.key) { switch (event.key) {
case 'a':
console.log('a');
select(document.activeElement);
break;
case 'b': case 'b':
console.log('b'); console.log('b');
closeOrExit(); closeOrExit();
@ -58,6 +72,14 @@ if (isNx) {
console.log('o'); console.log('o');
cyclePrevTab(); cyclePrevTab();
break; break;
case 'ArrowLeft':
console.log('ArrowLeft');
decreaseSelectedHandle();
break;
case 'ArrowRight':
console.log('ArrowRight');
increaseSelectedHandle();
break;
} }
}); });
} }
@ -65,6 +87,13 @@ if (isNx) {
const onLoad = () => { const onLoad = () => {
// Activate the first tab // Activate the first tab
openTab(document.querySelector('button.tab-button')); openTab(document.querySelector('button.tab-button'));
initializeAllSliders();
if (isNx) {
window.nx.sendMessage("loaded");
} else {
settings = {};
setSettingsFromJSON("{\"menu\":{\"aerial_delay\":0,\"air_dodge_dir\":0,\"attack_angle\":0,\"buff_state\":0,\"character_item\":0,\"clatter_strength\":0,\"crouch\":0,\"di_state\":0,\"falling_aerials\":0,\"fast_fall_delay\":0,\"fast_fall\":0,\"follow_up\":0,\"frame_advantage\":0,\"full_hop\":0,\"hitbox_vis\":1,\"input_delay\":1,\"ledge_delay\":0,\"ledge_state\":31,\"mash_state\":0,\"mash_triggers\":131,\"miss_tech_state\":15,\"oos_offset\":0,\"pummel_delay\":0,\"quick_menu\":0,\"reaction_time\":0,\"save_damage\":4,\"save_damage_limits\":[63,106],\"save_state_autoload\":1,\"save_state_enable\":1,\"save_state_mirroring\":1,\"sdi_state\":0,\"sdi_strength\":0,\"shield_state\":0,\"shield_tilt\":0,\"stage_hazards\":0,\"tech_state\":15,\"throw_delay\":0,\"throw_state\":1},\"defaults_menu\":{\"aerial_delay\":0,\"air_dodge_dir\":0,\"attack_angle\":0,\"buff_state\":0,\"character_item\":0,\"clatter_strength\":0,\"crouch\":0,\"di_state\":0,\"falling_aerials\":0,\"fast_fall_delay\":0,\"fast_fall\":0,\"follow_up\":0,\"frame_advantage\":0,\"full_hop\":0,\"hitbox_vis\":1,\"input_delay\":1,\"ledge_delay\":0,\"ledge_state\":31,\"mash_state\":0,\"mash_triggers\":131,\"miss_tech_state\":15,\"oos_offset\":0,\"pummel_delay\":0,\"quick_menu\":0,\"reaction_time\":0,\"save_damage\":4,\"save_damage_limits\":[41,118],\"save_state_autoload\":1,\"save_state_enable\":1,\"save_state_mirroring\":1,\"sdi_state\":0,\"sdi_strength\":0,\"shield_state\":0,\"shield_tilt\":0,\"stage_hazards\":0,\"tech_state\":15,\"throw_delay\":0,\"throw_state\":1}}");
}
}; };
window.onload = onLoad; window.onload = onLoad;
@ -73,6 +102,8 @@ var settings;
var defaultSettings; var defaultSettings;
var lastFocusedItem = document.querySelector('.menu-item > button'); var lastFocusedItem = document.querySelector('.menu-item > button');
var selectedSliderHandle = -1;
const currentTabContent = () => { const currentTabContent = () => {
const currentActiveTab = document.querySelector('.tab-button.active'); const currentActiveTab = document.querySelector('.tab-button.active');
@ -114,7 +145,11 @@ const openMenuItem = (eventTarget) => {
currentTabContent().classList.toggle('hide'); currentTabContent().classList.toggle('hide');
modal.classList.toggle('hide'); modal.classList.toggle('hide');
modal.querySelector('button').focus(); elem = modal.querySelector('button');
if (!elem) {
elem = modal.querySelector('.noUi-handle-lower')
}
elem.focus();
lastFocusedItem = eventTarget; lastFocusedItem = eventTarget;
}; };
@ -195,6 +230,19 @@ const exit = () => {
}; };
function closeOrExit() { function closeOrExit() {
// Deselect any sliders
handlesWereSelected = deselectSliderHandles();
if (handlesWereSelected) {return}
selectedHandles = document.querySelectorAll(".handleSelected");
if (selectedHandles.length) {
console.log("Found selected handles");
selectedHandles.forEach((handle) => {
handle.classList.remove("handleSelected");
});
return;
}
// Close any open menus // Close any open menus
if (document.querySelector('.modal:not(.hide)')) { if (document.querySelector('.modal:not(.hide)')) {
console.log('Closing Items'); console.log('Closing Items');
@ -210,52 +258,12 @@ function closeOrExit() {
function setSettingsFromJSON(msg) { function setSettingsFromJSON(msg) {
// Receive a menu message and set settings // Receive a menu message and set settings
var msg_json = JSON.parse(msg.data); var msg_json = JSON.parse(msg);
settings = msg_json["menu"]; settings = msg_json["menu"];
defaultSettings = msg_json["defaults_menu"]; defaultSettings = msg_json["defaults_menu"];
populateMenuFromSettings(); populateMenuFromSettings();
} }
function setSettingsFromURL() {
var { search } = window.location;
// Actual settings
const settingsFromSearch = search
.replace('?', '')
.split('&')
.reduce((accumulator, currentValue) => {
var [key, value] = currentValue.split('=');
if (!key.startsWith('__')) {
accumulator[key] = parseInt(value);
}
return accumulator;
}, {});
settings = settingsFromSearch;
// Default settings
const defaultSettingsFromSearch = search
.replace('?', '')
.split('&')
.reduce((accumulator, currentValue) => {
var [key, value] = currentValue.split('=');
if (key.startsWith('__')) {
accumulator[key.replace('__','')] = parseInt(value);
}
return accumulator;
}, {});
defaultSettings = defaultSettingsFromSearch;
populateMenuFromSettings()
}
function buildURLFromSettings() {
const url = 'http://localhost/?';
const urlParams = Object.entries(settings).map((setting) => {
return `${setting[0]}=${setting[1]}`;
});
return url + urlParams.join('&');
}
function selectSingleOption(eventTarget) { function selectSingleOption(eventTarget) {
// Deselect all options in the submenu // Deselect all options in the submenu
const parent = eventTarget.parentElement; const parent = eventTarget.parentElement;
@ -277,19 +285,26 @@ const isValueInBitmask = (value, mask) => (mask & value) != 0;
const setOptionsForMenu = (menuId) => { const setOptionsForMenu = (menuId) => {
const modal = document.querySelector(`.modal[data-id="${menuId}"]`); const modal = document.querySelector(`.modal[data-id="${menuId}"]`);
modal.querySelectorAll('.menu-icon').forEach(function (toggle) { if (modal.querySelector('.modal-button')) {
if (isValueInBitmask(toggle.dataset.val, settings[menuId])) { // Toggle menu
toggle.classList.remove('hidden'); modal.querySelectorAll('.menu-icon').forEach(function (toggle) {
} else { if (isValueInBitmask(toggle.dataset.val, settings[menuId])) {
toggle.classList.add('hidden'); toggle.classList.remove('hidden');
} } else {
}); toggle.classList.add('hidden');
}
});
if (modal.classList.contains('single-option')) { if (modal.classList.contains('single-option')) {
// If no option is selected default to the first option // If no option is selected default to the first option
if (modal.querySelectorAll('.menu-icon:not(.hidden)').length === 0) { if (modal.querySelectorAll('.menu-icon:not(.hidden)').length === 0) {
selectSingleOption(modal.querySelector('button')); selectSingleOption(modal.querySelector('button'));
}
} }
} else {
// Slider menu
slider = modal.querySelector('.modal-slider');
setSliderVals(slider, settings[menuId]);
} }
}; };
@ -297,17 +312,25 @@ function populateMenuFromSettings() {
document.querySelectorAll('.menu-item').forEach((item) => setOptionsForMenu(item.id)); document.querySelectorAll('.menu-item').forEach((item) => setOptionsForMenu(item.id));
} }
function getMaskFromMenuID(id) { function getSettingsValFromMenuID(id) {
var value = 0;
const modal = document.querySelector(`.modal[data-id='${id}']`); const modal = document.querySelector(`.modal[data-id='${id}']`);
const options = modal.querySelectorAll('img:not(.hidden)'); if (modal.querySelector('.modal-button')) {
// Toggle menu
// Return value is a bitmask
var value = 0;
const options = modal.querySelectorAll('img:not(.hidden)');
options.forEach(function (toggle) { options.forEach(function (toggle) {
value += parseInt(toggle.dataset.val); value += parseInt(toggle.dataset.val);
}); });
return value;
return value; } else {
// Slider menu
// Return value is a [lower,upper] array
slider = modal.querySelector('.modal-slider');
return getSliderVals(slider);
}
} }
function resetCurrentMenu() { function resetCurrentMenu() {
@ -315,10 +338,11 @@ function resetCurrentMenu() {
const menu = document.querySelector('.modal:not(.hide)'); const menu = document.querySelector('.modal:not(.hide)');
const menuId = menu.dataset.id; const menuId = menu.dataset.id;
const defaultSectionMask = defaultSettings[menuId]; const defaultSubmenuSetting = defaultSettings[menuId];
settings[menuId] = defaultSectionMask; settings[menuId] = defaultSubmenuSetting;
deselectSliderHandles();
populateMenuFromSettings(); populateMenuFromSettings();
} }
@ -327,10 +351,11 @@ function resetAllMenus() {
if (confirm('Are you sure that you want to reset all menu settings to the default?')) { if (confirm('Are you sure that you want to reset all menu settings to the default?')) {
document.querySelectorAll('.menu-item').forEach(function (item) { document.querySelectorAll('.menu-item').forEach(function (item) {
const defaultMenuId = item.id; const defaultMenuId = item.id;
const defaultMask = defaultSettings[defaultMenuId]; const defaultSubmenuSetting = defaultSettings[defaultMenuId];
settings[item.id] = defaultMask; settings[item.id] = defaultSubmenuSetting;
deselectSliderHandles();
populateMenuFromSettings(); populateMenuFromSettings();
}); });
} }
@ -344,13 +369,13 @@ function saveDefaults() {
if (confirm('Are you sure that you want to change the default menu settings to the current selections?')) { if (confirm('Are you sure that you want to change the default menu settings to the current selections?')) {
document.querySelectorAll('.menu-item').forEach((item) => { document.querySelectorAll('.menu-item').forEach((item) => {
const menu = item.id; const menu = item.id;
defaultSettings[menu] = getSettingsValFromMenuID(item.id);
defaultSettings[menu] = getMaskFromMenuID(item.id);
}); });
} }
} }
function cycleNextTab() { function cycleNextTab() {
deselectSliderHandles();
// Cycle to the next tab // Cycle to the next tab
const activeTab = document.querySelector('.tab-button.active'); const activeTab = document.querySelector('.tab-button.active');
var nextTab = activeTab.nextElementSibling; var nextTab = activeTab.nextElementSibling;
@ -362,6 +387,7 @@ function cycleNextTab() {
} }
function cyclePrevTab() { function cyclePrevTab() {
deselectSliderHandles();
// Cycle to the previous tab // Cycle to the previous tab
const activeTab = document.querySelector('.tab-button.active'); const activeTab = document.querySelector('.tab-button.active');
var previousTab = activeTab.previousElementSibling; var previousTab = activeTab.previousElementSibling;
@ -372,3 +398,124 @@ function cyclePrevTab() {
} }
openTab(previousTab); openTab(previousTab);
} }
function getSliderVals(slider) {
var arr = slider.noUiSlider.get();
return [parseFloat(arr[0]), parseFloat(arr[1])]
}
function setSliderVals(slider, vals) {
slider.noUiSlider.set(vals);
}
function setSettingsFromSlider(slider) {
menuId = closestClass(slider, "modal").dataset.id;
settings[menuId] = getSliderVals(slider)
}
function initializeSlider(slider) {
noUiSlider.create(
slider,
{
start: [
parseFloat(slider.dataset.selectedMin),
parseFloat(slider.dataset.selectedMax),
],
connect: true,
range: {
'min': parseFloat(slider.dataset.absMin),
'max': parseFloat(slider.dataset.absMax),
},
step: 1,
tooltips: [
{ to: function (value) { return value.toFixed(0) + '%'; } },
{ to: function (value) { return value.toFixed(0) + '%'; } },
],
pips: {
mode: 'range',
density: 10,
},
keyboardMultiplier: 0, // Prevents doublestepping with custom implementation
}
);
slider.noUiSlider.on('set', function () { setSettingsFromSlider(slider) });
}
function initializeAllSliders() {
document.querySelectorAll(".modal-slider").forEach((item) => {
initializeSlider(item);
});
}
function select(element) {
if (element.classList.contains("noUi-handle")) {
element.classList.toggle("handleSelected");
}
element.click();
}
function increaseSelectedHandle() {
// Increments the selected slider handle, if one is selected
// Won't go past the slider limit
handle = document.querySelector(".noUi-handle.handleSelected");
if (handle) {
slider = closestClass(handle, "modal-slider");
isLowerHandle = handle.classList.contains("noUi-handle-lower");
step = slider.noUiSlider.options.step;
currentVals = getSliderVals(slider);
if (isLowerHandle) {
setSliderVals(
slider,
[currentVals[0] + step, null]
);
} else {
setSliderVals(
slider,
[null, currentVals[1] + step]
);
}
// Refocus the handle, since the native navigation might focus the other handle
// TODO: Is there a more elegant way to do this?
setTimeout( function() {handle.focus() }, 15);
}
}
function decreaseSelectedHandle() {
// Decrements the selected slider handle, if one is selected
// Won't go past the slider limit
handle = document.querySelector(".noUi-handle.handleSelected");
if (handle) {
slider = closestClass(handle, "modal-slider");
isLowerHandle = handle.classList.contains("noUi-handle-lower");
step = slider.noUiSlider.options.step;
currentVals = getSliderVals(slider);
if (isLowerHandle) {
setSliderVals(
slider,
[currentVals[0] - step, null]
);
} else {
setSliderVals(
slider,
[null, currentVals[1] - step]
);
}
// Refocus the handle, since the native navigation might focus the other handle
// TODO: Is there a more elegant way to do this?
setTimeout( function() {handle.focus() }, 15);
}
}
function deselectSliderHandles() {
/// Returns true if any slider handles were changed from selected -> deselected
/// Returns false if there were no selected slider handles to begin with
selectedHandles = document.querySelectorAll(".handleSelected");
if (selectedHandles.length) {
selectedHandles.forEach((handle) => {
handle.classList.remove("handleSelected");
});
return true;
} else {
return false;
}
}

View file

@ -5,10 +5,12 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no"> <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>Modpack Menu</title> <title>Modpack Menu</title>
<link rel="stylesheet" href="./css/nouislider.min.css">
<link rel="stylesheet" href="./css/training_modpack.css" /> <link rel="stylesheet" href="./css/training_modpack.css" />
</head> </head>
<body> <body>
<script defer src="./js/nouislider.min.js"></script>
<script defer src="./js/training_modpack.js"></script> <script defer src="./js/training_modpack.js"></script>
<div class="header"> <div class="header">
<a id="ret-button" tabindex="-1" class="return-icon-container" onclick="closeOrExit()"> <a id="ret-button" tabindex="-1" class="return-icon-container" onclick="closeOrExit()">
@ -42,6 +44,20 @@
</div> </div>
</button> </button>
{{/toggles}} {{/toggles}}
{{#slider}}
<div class="modal-slider-label">
<p>{{submenu_title}}</p>
</div>
<div
id="{{submenu_id}}-slider"
data-selected-min="{{selected_min}}"
data-selected-max="{{selected_max}}"
data-abs-min="{{abs_min}}"
data-abs-max="{{abs_max}}"
class="modal-slider"
>
</div>
{{/slider}}
</div> </div>
{{/tab_submenus}} {{/tab_submenus}}
<div id="{{tab_id}}_tab" class="tab-content hide"> <div id="{{tab_id}}_tab" class="tab-content hide">

View file

@ -1,4 +1,5 @@
use crate::common::button_config; use crate::common::button_config;
use crate::common::consts::get_random_float;
use crate::common::consts::get_random_int; use crate::common::consts::get_random_int;
use crate::common::consts::FighterId; use crate::common::consts::FighterId;
use crate::common::consts::OnOff; use crate::common::consts::OnOff;
@ -15,7 +16,7 @@ use smash::app::{self, lua_bind::*, Item};
use smash::hash40; use smash::hash40;
use smash::lib::lua_const::*; use smash::lib::lua_const::*;
use smash::phx::{Hash40, Vector3f}; use smash::phx::{Hash40, Vector3f};
use training_mod_consts::CharacterItem; use training_mod_consts::{CharacterItem, SaveDamage};
#[derive(PartialEq)] #[derive(PartialEq)]
enum SaveState { enum SaveState {
@ -149,16 +150,6 @@ pub unsafe fn get_param_int(
} }
fn set_damage(module_accessor: &mut app::BattleObjectModuleAccessor, damage: f32) { fn set_damage(module_accessor: &mut app::BattleObjectModuleAccessor, damage: f32) {
let overwrite_damage;
unsafe {
overwrite_damage = MENU.save_damage == OnOff::On;
}
if !overwrite_damage {
return;
}
unsafe { unsafe {
DamageModule::heal( DamageModule::heal(
module_accessor, module_accessor,
@ -365,7 +356,41 @@ pub unsafe fn save_states(module_accessor: &mut app::BattleObjectModuleAccessor)
// If we're done moving, reset percent, handle charges, and apply buffs // If we're done moving, reset percent, handle charges, and apply buffs
if save_state.state == NoAction { if save_state.state == NoAction {
set_damage(module_accessor, save_state.percent); // Set damage of the save state
if !is_cpu {
match MENU.save_damage_player {
SaveDamage::SAVED => {
set_damage(module_accessor, save_state.percent);
}
SaveDamage::RANDOM => {
// Gen random value
let pct: f32 = get_random_float(
MENU.save_damage_limits_player.0 as f32,
MENU.save_damage_limits_player.1 as f32,
);
set_damage(module_accessor, pct);
}
SaveDamage::DEFAULT => {}
_ => {}
}
} else {
match MENU.save_damage_cpu {
SaveDamage::SAVED => {
set_damage(module_accessor, save_state.percent);
}
SaveDamage::RANDOM => {
// Gen random value
let pct: f32 = get_random_float(
MENU.save_damage_limits_cpu.0 as f32,
MENU.save_damage_limits_cpu.1 as f32,
);
set_damage(module_accessor, pct);
}
SaveDamage::DEFAULT => {}
_ => {}
}
}
// Set to held item // Set to held item
if !is_cpu && !fighter_is_nana && MENU.character_item != CharacterItem::None { if !is_cpu && !fighter_is_nana && MENU.character_item != CharacterItem::None {
apply_item(MENU.character_item); apply_item(MENU.character_item);

View file

@ -1,4 +1,3 @@
#![feature(const_option)]
#[macro_use] #[macro_use]
extern crate bitflags; extern crate bitflags;
@ -14,13 +13,16 @@ use serde::{Deserialize, Serialize};
use serde_repr::{Deserialize_repr, Serialize_repr}; use serde_repr::{Deserialize_repr, Serialize_repr};
#[cfg(feature = "smash")] #[cfg(feature = "smash")]
use smash::lib::lua_const::*; use smash::lib::lua_const::*;
use std::collections::HashMap;
use strum::IntoEnumIterator; use strum::IntoEnumIterator;
use strum_macros::EnumIter; use strum_macros::EnumIter;
pub trait ToggleTrait { pub trait ToggleTrait {
fn to_toggle_strs() -> Vec<&'static str>; fn to_toggle_strs() -> Vec<&'static str>;
fn to_toggle_vals() -> Vec<usize>; fn to_toggle_vals() -> Vec<u32>;
}
pub trait SliderTrait {
fn get_limits() -> (u32, u32);
} }
// bitflag helper function macro // bitflag helper function macro
@ -73,9 +75,9 @@ macro_rules! extra_bitflag_impls {
all_options.iter().map(|i| i.as_str().unwrap_or("")).collect() all_options.iter().map(|i| i.as_str().unwrap_or("")).collect()
} }
fn to_toggle_vals() -> Vec<usize> { fn to_toggle_vals() -> Vec<u32> {
let all_options = <$e>::all().to_vec(); let all_options = <$e>::all().to_vec();
all_options.iter().map(|i| i.bits() as usize).collect() all_options.iter().map(|i| i.bits() as u32).collect()
} }
} }
} }
@ -91,6 +93,18 @@ pub fn get_random_int(_max: i32) -> i32 {
0 0
} }
/// Generate a random float between _min and _max.
/// Note that (_min <= _max) is not enforced.
pub fn get_random_float(_min: f32, _max: f32) -> f32 {
#[cfg(feature = "smash")]
unsafe {
_min + smash::app::sv_math::randf(smash::hash40("fighter"), _max - _min)
}
#[cfg(not(feature = "smash"))]
_min
}
pub fn random_option<T>(arg: &[T]) -> &T { pub fn random_option<T>(arg: &[T]) -> &T {
&arg[get_random_int(arg.len() as i32) as usize] &arg[get_random_int(arg.len() as i32) as usize]
} }
@ -289,8 +303,8 @@ impl ToggleTrait for Shield {
Shield::iter().map(|i| i.as_str().unwrap_or("")).collect() Shield::iter().map(|i| i.as_str().unwrap_or("")).collect()
} }
fn to_toggle_vals() -> Vec<usize> { fn to_toggle_vals() -> Vec<u32> {
Shield::iter().map(|i| i as usize).collect() Shield::iter().map(|i| i as u32).collect()
} }
} }
@ -322,8 +336,8 @@ impl ToggleTrait for SaveStateMirroring {
.collect() .collect()
} }
fn to_toggle_vals() -> Vec<usize> { fn to_toggle_vals() -> Vec<u32> {
SaveStateMirroring::iter().map(|i| i as usize).collect() SaveStateMirroring::iter().map(|i| i as u32).collect()
} }
} }
@ -355,7 +369,7 @@ impl ToggleTrait for OnOff {
fn to_toggle_strs() -> Vec<&'static str> { fn to_toggle_strs() -> Vec<&'static str> {
vec!["Off", "On"] vec!["Off", "On"]
} }
fn to_toggle_vals() -> Vec<usize> { fn to_toggle_vals() -> Vec<u32> {
vec![0, 1] vec![0, 1]
} }
} }
@ -875,8 +889,8 @@ impl ToggleTrait for InputFrequency {
.collect() .collect()
} }
fn to_toggle_vals() -> Vec<usize> { fn to_toggle_vals() -> Vec<u32> {
InputFrequency::iter().map(|i| i as usize).collect() InputFrequency::iter().map(|i| i as u32).collect()
} }
} }
@ -940,8 +954,8 @@ impl ToggleTrait for CharacterItem {
.collect() .collect()
} }
fn to_toggle_vals() -> Vec<usize> { fn to_toggle_vals() -> Vec<u32> {
CharacterItem::iter().map(|i| i as usize).collect() CharacterItem::iter().map(|i| i as u32).collect()
} }
} }
@ -993,17 +1007,56 @@ impl MashTrigger {
const fn default() -> MashTrigger { const fn default() -> MashTrigger {
// Hit, block, clatter // Hit, block, clatter
MashTrigger::HIT.union(MashTrigger::BLOCK).union(MashTrigger::CLATTER) MashTrigger::HIT
.union(MashTrigger::BLOCK)
.union(MashTrigger::CLATTER)
} }
} }
extra_bitflag_impls! {MashTrigger} extra_bitflag_impls! {MashTrigger}
impl_serde_for_bitflags!(MashTrigger); impl_serde_for_bitflags!(MashTrigger);
#[derive(Clone, Copy, Serialize, Deserialize, Debug)]
pub struct DamagePercent(pub u32, pub u32);
impl SliderTrait for DamagePercent {
fn get_limits() -> (u32, u32) {
(0, 150)
}
}
impl DamagePercent {
const fn default() -> DamagePercent {
DamagePercent(0, 150)
}
}
bitflags! {
pub struct SaveDamage : u32
{
const DEFAULT = 0b001;
const SAVED = 0b010;
const RANDOM = 0b100;
}
}
impl SaveDamage {
fn as_str(self) -> Option<&'static str> {
Some(match self {
SaveDamage::DEFAULT => "Default",
SaveDamage::SAVED => "Save State",
SaveDamage::RANDOM => "Random Value",
_ => return None,
})
}
}
extra_bitflag_impls! {SaveDamage}
impl_serde_for_bitflags!(SaveDamage);
#[repr(C)] #[repr(C)]
#[derive(Clone, Copy, Serialize, Deserialize, Debug)] #[derive(Clone, Copy, Serialize, Deserialize, Debug)]
pub struct TrainingModpackMenu { pub struct TrainingModpackMenu {
// Mash Tab
pub aerial_delay: Delay, pub aerial_delay: Delay,
pub air_dodge_dir: Direction, pub air_dodge_dir: Direction,
pub attack_angle: AttackAngle, pub attack_angle: AttackAngle,
@ -1029,7 +1082,10 @@ pub struct TrainingModpackMenu {
pub pummel_delay: MedDelay, pub pummel_delay: MedDelay,
pub quick_menu: OnOff, pub quick_menu: OnOff,
pub reaction_time: Delay, pub reaction_time: Delay,
pub save_damage: OnOff, pub save_damage_cpu: SaveDamage,
pub save_damage_limits_cpu: DamagePercent,
pub save_damage_player: SaveDamage,
pub save_damage_limits_player: DamagePercent,
pub save_state_autoload: OnOff, pub save_state_autoload: OnOff,
pub save_state_enable: OnOff, pub save_state_enable: OnOff,
pub save_state_mirroring: SaveStateMirroring, pub save_state_mirroring: SaveStateMirroring,
@ -1043,71 +1099,15 @@ pub struct TrainingModpackMenu {
pub throw_state: ThrowOption, pub throw_state: ThrowOption,
} }
macro_rules! set_by_str { const fn num_bits<T>() -> u32 {
($obj:ident, $s:ident, $($field:ident = $rhs:expr,)*) => { (std::mem::size_of::<T>() * 8) as u32
$(
if $s == stringify!($field) {
$obj.$field = $rhs.unwrap();
}
)*
}
}
const fn num_bits<T>() -> usize {
std::mem::size_of::<T>() * 8
} }
fn log_2(x: u32) -> u32 { fn log_2(x: u32) -> u32 {
if x == 0 { if x == 0 {
0 0
} else { } else {
num_bits::<u32>() as u32 - x.leading_zeros() - 1 num_bits::<u32>() - x.leading_zeros() - 1
}
}
impl TrainingModpackMenu {
pub fn set(&mut self, s: &str, val: u32) {
set_by_str!(
self,
s,
aerial_delay = Delay::from_bits(val),
air_dodge_dir = Direction::from_bits(val),
attack_angle = AttackAngle::from_bits(val),
clatter_strength = num::FromPrimitive::from_u32(val),
crouch = OnOff::from_val(val),
di_state = Direction::from_bits(val),
falling_aerials = BoolFlag::from_bits(val),
fast_fall_delay = Delay::from_bits(val),
fast_fall = BoolFlag::from_bits(val),
follow_up = Action::from_bits(val),
full_hop = BoolFlag::from_bits(val),
hitbox_vis = OnOff::from_val(val),
input_delay = Delay::from_bits(val),
ledge_delay = LongDelay::from_bits(val),
ledge_state = LedgeOption::from_bits(val),
mash_state = Action::from_bits(val),
mash_triggers = MashTrigger::from_bits(val),
miss_tech_state = MissTechFlags::from_bits(val),
oos_offset = Delay::from_bits(val),
reaction_time = Delay::from_bits(val),
sdi_state = Direction::from_bits(val),
sdi_strength = num::FromPrimitive::from_u32(val),
shield_state = num::FromPrimitive::from_u32(val),
shield_tilt = Direction::from_bits(val),
stage_hazards = OnOff::from_val(val),
tech_state = TechFlags::from_bits(val),
save_damage = OnOff::from_val(val),
frame_advantage = OnOff::from_val(val),
save_state_mirroring = num::FromPrimitive::from_u32(val),
save_state_enable = OnOff::from_val(val),
save_state_autoload = OnOff::from_val(val),
throw_state = ThrowOption::from_bits(val),
throw_delay = MedDelay::from_bits(val),
pummel_delay = MedDelay::from_bits(val),
buff_state = BuffOption::from_bits(val),
character_item = num::FromPrimitive::from_u32(val),
quick_menu = OnOff::from_val(val),
);
} }
} }
@ -1169,7 +1169,10 @@ pub static DEFAULTS_MENU: TrainingModpackMenu = TrainingModpackMenu {
pummel_delay: MedDelay::empty(), pummel_delay: MedDelay::empty(),
quick_menu: OnOff::Off, quick_menu: OnOff::Off,
reaction_time: Delay::empty(), reaction_time: Delay::empty(),
save_damage: OnOff::On, save_damage_cpu: SaveDamage::DEFAULT,
save_damage_limits_cpu: DamagePercent::default(),
save_damage_player: SaveDamage::DEFAULT,
save_damage_limits_player: DamagePercent::default(),
save_state_autoload: OnOff::Off, save_state_autoload: OnOff::Off,
save_state_enable: OnOff::On, save_state_enable: OnOff::On,
save_state_mirroring: SaveStateMirroring::None, save_state_mirroring: SaveStateMirroring::None,
@ -1185,37 +1188,38 @@ pub static DEFAULTS_MENU: TrainingModpackMenu = TrainingModpackMenu {
pub static mut MENU: TrainingModpackMenu = DEFAULTS_MENU; pub static mut MENU: TrainingModpackMenu = DEFAULTS_MENU;
#[derive(Content, Clone)] #[derive(Content, Clone, Serialize)]
pub struct Slider { pub struct Slider {
pub min: usize, pub selected_min: u32,
pub max: usize, pub selected_max: u32,
pub index: usize, pub abs_min: u32,
pub value: usize, pub abs_max: u32,
} }
#[derive(Content, Clone)] #[derive(Content, Clone, Serialize)]
pub struct Toggle<'a> { pub struct Toggle<'a> {
pub toggle_value: usize, pub toggle_value: u32,
pub toggle_title: &'a str, pub toggle_title: &'a str,
pub checked: bool, pub checked: bool,
} }
#[derive(Content, Clone)] #[derive(Content, Clone, Serialize)]
pub struct SubMenu<'a> { pub struct SubMenu<'a> {
pub submenu_title: &'a str, pub submenu_title: &'a str,
pub submenu_id: &'a str, pub submenu_id: &'a str,
pub help_text: &'a str, pub help_text: &'a str,
pub is_single_option: bool, pub is_single_option: bool,
pub toggles: Vec<Toggle<'a>>, pub toggles: Vec<Toggle<'a>>,
pub slider: Option<Slider>,
pub _type: &'a str, pub _type: &'a str,
} }
impl<'a> SubMenu<'a> { impl<'a> SubMenu<'a> {
pub fn add_toggle(&mut self, toggle_value: usize, toggle_title: &'a str) { pub fn add_toggle(&mut self, toggle_value: u32, toggle_title: &'a str, checked: bool) {
self.toggles.push(Toggle { self.toggles.push(Toggle {
toggle_value: toggle_value, toggle_value,
toggle_title: toggle_title, toggle_title,
checked: false, checked,
}); });
} }
pub fn new_with_toggles<T: ToggleTrait>( pub fn new_with_toggles<T: ToggleTrait>(
@ -1223,6 +1227,7 @@ impl<'a> SubMenu<'a> {
submenu_id: &'a str, submenu_id: &'a str,
help_text: &'a str, help_text: &'a str,
is_single_option: bool, is_single_option: bool,
initial_value: &u32
) -> SubMenu<'a> { ) -> SubMenu<'a> {
let mut instance = SubMenu { let mut instance = SubMenu {
submenu_title: submenu_title, submenu_title: submenu_title,
@ -1230,19 +1235,45 @@ impl<'a> SubMenu<'a> {
help_text: help_text, help_text: help_text,
is_single_option: is_single_option, is_single_option: is_single_option,
toggles: Vec::new(), toggles: Vec::new(),
slider: None,
_type: "toggle", _type: "toggle",
}; };
let values = T::to_toggle_vals(); let values = T::to_toggle_vals();
let titles = T::to_toggle_strs(); let titles = T::to_toggle_strs();
for i in 0..values.len() { for i in 0..values.len() {
instance.add_toggle(values[i], titles[i]); let checked: bool = (values[i] & initial_value) > 0
|| (!values[i] == 0 && initial_value == &0);
instance.add_toggle(values[i], titles[i], checked);
} }
instance instance
} }
pub fn new_with_slider<S: SliderTrait>(
submenu_title: &'a str,
submenu_id: &'a str,
help_text: &'a str,
initial_lower_value: &u32,
initial_upper_value: &u32,
) -> SubMenu<'a> {
let min_max = S::get_limits();
SubMenu {
submenu_title: submenu_title,
submenu_id: submenu_id,
help_text: help_text,
is_single_option: false,
toggles: Vec::new(),
slider: Some(Slider {
selected_min: *initial_lower_value,
selected_max: *initial_upper_value,
abs_min: min_max.0,
abs_max: min_max.1,
}),
_type: "slider",
}
}
} }
#[derive(Content)] #[derive(Content, Serialize)]
pub struct Tab<'a> { pub struct Tab<'a> {
pub tab_id: &'a str, pub tab_id: &'a str,
pub tab_title: &'a str, pub tab_title: &'a str,
@ -1256,17 +1287,36 @@ impl<'a> Tab<'a> {
submenu_id: &'a str, submenu_id: &'a str,
help_text: &'a str, help_text: &'a str,
is_single_option: bool, is_single_option: bool,
initial_value: &u32,
) { ) {
self.tab_submenus.push(SubMenu::new_with_toggles::<T>( self.tab_submenus.push(SubMenu::new_with_toggles::<T>(
submenu_title, submenu_title,
submenu_id, submenu_id,
help_text, help_text,
is_single_option, is_single_option,
initial_value,
)); ));
} }
pub fn add_submenu_with_slider<S: SliderTrait>(
&mut self,
submenu_title: &'a str,
submenu_id: &'a str,
help_text: &'a str,
initial_lower_value: &u32,
initial_upper_value: &u32,
) {
self.tab_submenus.push(SubMenu::new_with_slider::<S>(
submenu_title,
submenu_id,
help_text,
initial_lower_value,
initial_upper_value,
))
}
} }
#[derive(Content)] #[derive(Content, Serialize)]
pub struct UiMenu<'a> { pub struct UiMenu<'a> {
pub tabs: Vec<Tab<'a>>, pub tabs: Vec<Tab<'a>>,
} }
@ -1284,84 +1334,98 @@ pub unsafe fn get_menu() -> UiMenu<'static> {
"mash_state", "mash_state",
"Mash Toggles: Actions to be performed as soon as possible", "Mash Toggles: Actions to be performed as soon as possible",
false, false,
&(MENU.mash_state.bits as u32),
); );
mash_tab.add_submenu_with_toggles::<Action>( mash_tab.add_submenu_with_toggles::<Action>(
"Followup Toggles", "Followup Toggles",
"follow_up", "follow_up",
"Followup Toggles: Actions to be performed after the Mash option", "Followup Toggles: Actions to be performed after the Mash option",
false, false,
&(MENU.follow_up.bits as u32),
); );
mash_tab.add_submenu_with_toggles::<MashTrigger>( mash_tab.add_submenu_with_toggles::<MashTrigger>(
"Mash Triggers", "Mash Triggers",
"mash_triggers", "mash_triggers",
"Mash triggers: When the Mash Option will be performed", "Mash triggers: When the Mash Option will be performed",
false, false,
&(MENU.mash_triggers.bits as u32),
); );
mash_tab.add_submenu_with_toggles::<AttackAngle>( mash_tab.add_submenu_with_toggles::<AttackAngle>(
"Attack Angle", "Attack Angle",
"attack_angle", "attack_angle",
"Attack Angle: For attacks that can be angled, such as some forward tilts", "Attack Angle: For attacks that can be angled, such as some forward tilts",
false, false,
&(MENU.attack_angle.bits as u32),
); );
mash_tab.add_submenu_with_toggles::<ThrowOption>( mash_tab.add_submenu_with_toggles::<ThrowOption>(
"Throw Options", "Throw Options",
"throw_state", "throw_state",
"Throw Options: Throw to be performed when a grab is landed", "Throw Options: Throw to be performed when a grab is landed",
false, false,
&(MENU.throw_state.bits as u32),
); );
mash_tab.add_submenu_with_toggles::<MedDelay>( mash_tab.add_submenu_with_toggles::<MedDelay>(
"Throw Delay", "Throw Delay",
"throw_delay", "throw_delay",
"Throw Delay: How many frames to delay the throw option", "Throw Delay: How many frames to delay the throw option",
false, false,
&(MENU.throw_delay.bits as u32),
); );
mash_tab.add_submenu_with_toggles::<MedDelay>( mash_tab.add_submenu_with_toggles::<MedDelay>(
"Pummel Delay", "Pummel Delay",
"pummel_delay", "pummel_delay",
"Pummel Delay: How many frames after a grab to wait before starting to pummel", "Pummel Delay: How many frames after a grab to wait before starting to pummel",
false, false,
&(MENU.pummel_delay.bits as u32),
); );
mash_tab.add_submenu_with_toggles::<BoolFlag>( mash_tab.add_submenu_with_toggles::<BoolFlag>(
"Falling Aerials", "Falling Aerials",
"falling_aerials", "falling_aerials",
"Falling Aerials: Should aerials be performed when rising or when falling", "Falling Aerials: Should aerials be performed when rising or when falling",
false, // TODO: Should this be a single option submenu? false,
&(MENU.falling_aerials.bits as u32),
); );
mash_tab.add_submenu_with_toggles::<BoolFlag>( mash_tab.add_submenu_with_toggles::<BoolFlag>(
"Full Hop", "Full Hop",
"full_hop", "full_hop",
"Full Hop: Should the CPU perform a full hop or a short hop", "Full Hop: Should the CPU perform a full hop or a short hop",
false, false,
&(MENU.full_hop.bits as u32),
); );
mash_tab.add_submenu_with_toggles::<Delay>( mash_tab.add_submenu_with_toggles::<Delay>(
"Aerial Delay", "Aerial Delay",
"aerial_delay", "aerial_delay",
"Aerial Delay: How long to delay a Mash aerial attack", "Aerial Delay: How long to delay a Mash aerial attack",
false, false,
&(MENU.aerial_delay.bits as u32),
); );
mash_tab.add_submenu_with_toggles::<BoolFlag>( mash_tab.add_submenu_with_toggles::<BoolFlag>(
"Fast Fall", "Fast Fall",
"fast_fall", "fast_fall",
"Fast Fall: Should the CPU fastfall during a jump", "Fast Fall: Should the CPU fastfall during a jump",
false, false,
&(MENU.fast_fall.bits as u32),
); );
mash_tab.add_submenu_with_toggles::<Delay>( mash_tab.add_submenu_with_toggles::<Delay>(
"Fast Fall Delay", "Fast Fall Delay",
"fast_fall_delay", "fast_fall_delay",
"Fast Fall Delay: How many frames the CPU should delay their fastfall", "Fast Fall Delay: How many frames the CPU should delay their fastfall",
false, false,
&(MENU.fast_fall_delay.bits as u32),
); );
mash_tab.add_submenu_with_toggles::<Delay>( mash_tab.add_submenu_with_toggles::<Delay>(
"OoS Offset", "OoS Offset",
"oos_offset", "oos_offset",
"OoS Offset: How many times the CPU shield can be hit before performing a Mash option", "OoS Offset: How many times the CPU shield can be hit before performing a Mash option",
false, false,
&(MENU.oos_offset.bits as u32),
); );
mash_tab.add_submenu_with_toggles::<Delay>( mash_tab.add_submenu_with_toggles::<Delay>(
"Reaction Time", "Reaction Time",
"reaction_time", "reaction_time",
"Reaction Time: How many frames to delay before performing a mash option", "Reaction Time: How many frames to delay before performing a mash option",
false, false,
&(MENU.reaction_time.bits as u32),
); );
overall_menu.tabs.push(mash_tab); overall_menu.tabs.push(mash_tab);
@ -1375,178 +1439,199 @@ pub unsafe fn get_menu() -> UiMenu<'static> {
"air_dodge_dir", "air_dodge_dir",
"Airdodge Direction: Direction to angle airdodges", "Airdodge Direction: Direction to angle airdodges",
false, false,
&(MENU.air_dodge_dir.bits as u32),
); );
defensive_tab.add_submenu_with_toggles::<Direction>( defensive_tab.add_submenu_with_toggles::<Direction>(
"DI Direction", "DI Direction",
"di_state", "di_state",
"DI Direction: Direction to angle the directional influence during hitlag", "DI Direction: Direction to angle the directional influence during hitlag",
false, false,
&(MENU.di_state.bits as u32),
); );
defensive_tab.add_submenu_with_toggles::<Direction>( defensive_tab.add_submenu_with_toggles::<Direction>(
"SDI Direction", "SDI Direction",
"sdi_state", "sdi_state",
"SDI Direction: Direction to angle the smash directional influence during hitlag", "SDI Direction: Direction to angle the smash directional influence during hitlag",
false, false,
&(MENU.sdi_state.bits as u32),
); );
defensive_tab.add_submenu_with_toggles::<InputFrequency>( defensive_tab.add_submenu_with_toggles::<InputFrequency>(
"SDI Strength", "SDI Strength",
"sdi_strength", "sdi_strength",
"SDI Strength: Relative strength of the smash directional influence inputs", "SDI Strength: Relative strength of the smash directional influence inputs",
true, true,
&(MENU.sdi_strength as u32),
); );
defensive_tab.add_submenu_with_toggles::<InputFrequency>( defensive_tab.add_submenu_with_toggles::<InputFrequency>(
"Clatter Strength", "Clatter Strength",
"clatter_strength", "clatter_strength",
"Clatter Strength: Relative strength of the mashing out of grabs, buries, etc.", "Clatter Strength: Relative strength of the mashing out of grabs, buries, etc.",
true, true,
&(MENU.clatter_strength as u32),
); );
defensive_tab.add_submenu_with_toggles::<LedgeOption>( defensive_tab.add_submenu_with_toggles::<LedgeOption>(
"Ledge Options", "Ledge Options",
"ledge_state", "ledge_state",
"Ledge Options: Actions to be taken when on the ledge", "Ledge Options: Actions to be taken when on the ledge",
false, false,
&(MENU.ledge_state.bits as u32),
); );
defensive_tab.add_submenu_with_toggles::<LongDelay>( defensive_tab.add_submenu_with_toggles::<LongDelay>(
"Ledge Delay", "Ledge Delay",
"ledge_delay", "ledge_delay",
"Ledge Delay: How many frames to delay the ledge option", "Ledge Delay: How many frames to delay the ledge option",
false, false,
&(MENU.ledge_delay.bits as u32),
); );
defensive_tab.add_submenu_with_toggles::<TechFlags>( defensive_tab.add_submenu_with_toggles::<TechFlags>(
"Tech Options", "Tech Options",
"tech_state", "tech_state",
"Tech Options: Actions to take when slammed into a hard surface", "Tech Options: Actions to take when slammed into a hard surface",
false, false,
&(MENU.tech_state.bits as u32),
); );
defensive_tab.add_submenu_with_toggles::<MissTechFlags>( defensive_tab.add_submenu_with_toggles::<MissTechFlags>(
"Mistech Options", "Mistech Options",
"miss_tech_state", "miss_tech_state",
"Mistech Options: Actions to take after missing a tech", "Mistech Options: Actions to take after missing a tech",
false, false,
&(MENU.miss_tech_state.bits as u32),
); );
defensive_tab.add_submenu_with_toggles::<Shield>( defensive_tab.add_submenu_with_toggles::<Shield>(
"Shield Toggles", "Shield Toggles",
"shield_state", "shield_state",
"Shield Toggles: CPU Shield Behavior", "Shield Toggles: CPU Shield Behavior",
true, true,
&(MENU.shield_state as u32),
); );
defensive_tab.add_submenu_with_toggles::<Direction>( defensive_tab.add_submenu_with_toggles::<Direction>(
"Shield Tilt", "Shield Tilt",
"shield_tilt", "shield_tilt",
"Shield Tilt: Direction to tilt the shield", "Shield Tilt: Direction to tilt the shield",
false, // TODO: Should this be true? false, // TODO: Should this be true?
&(MENU.shield_tilt.bits as u32),
); );
defensive_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,
);
defensive_tab.add_submenu_with_toggles::<CharacterItem>(
"Character Item",
"character_item",
"Character Item: CPU/Player item to hold when loading a save state",
true,
);
defensive_tab.add_submenu_with_toggles::<OnOff>( defensive_tab.add_submenu_with_toggles::<OnOff>(
"Crouch", "Crouch",
"crouch", "crouch",
"Crouch: Should the CPU crouch when on the ground", "Crouch: Should the CPU crouch when on the ground",
true, true,
&(MENU.crouch as u32),
); );
overall_menu.tabs.push(defensive_tab); overall_menu.tabs.push(defensive_tab);
let mut save_state_tab = Tab {
tab_id: "save_state",
tab_title: "Save States",
tab_submenus: Vec::new(),
};
save_state_tab.add_submenu_with_toggles::<SaveStateMirroring>(
"Mirroring",
"save_state_mirroring",
"Mirroring: Flips save states in the left-right direction across the stage center",
true,
&(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),
);
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),
);
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),
);
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),
);
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),
);
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),
);
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),
);
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),
);
overall_menu.tabs.push(save_state_tab);
let mut misc_tab = Tab { let mut misc_tab = Tab {
tab_id: "misc", tab_id: "misc",
tab_title: "Misc Settings", tab_title: "Misc Settings",
tab_submenus: Vec::new(), tab_submenus: Vec::new(),
}; };
misc_tab.add_submenu_with_toggles::<SaveStateMirroring>(
"Mirroring",
"save_state_mirroring",
"Mirroring: Flips save states in the left-right direction across the stage center",
true,
);
misc_tab.add_submenu_with_toggles::<OnOff>(
"Save Damage",
"save_damage",
"Save Damage: Should save states retain player/CPU damage",
true,
);
misc_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,
);
misc_tab.add_submenu_with_toggles::<OnOff>(
"Save States Autoload",
"save_state_autoload",
"Save States Autoload: Load save state when any fighter dies",
true,
);
misc_tab.add_submenu_with_toggles::<OnOff>( misc_tab.add_submenu_with_toggles::<OnOff>(
"Frame Advantage", "Frame Advantage",
"frame_advantage", "frame_advantage",
"Frame Advantage: Display the time difference between when the player is actionable and the CPU is actionable", "Frame Advantage: Display the time difference between when the player is actionable and the CPU is actionable",
true, true,
&(MENU.frame_advantage as u32),
); );
misc_tab.add_submenu_with_toggles::<OnOff>( misc_tab.add_submenu_with_toggles::<OnOff>(
"Hitbox Visualization", "Hitbox Visualization",
"hitbox_vis", "hitbox_vis",
"Hitbox Visualization: Should hitboxes be displayed, hiding other visual effects", "Hitbox Visualization: Should hitboxes be displayed, hiding other visual effects",
true, true,
&(MENU.hitbox_vis as u32),
); );
misc_tab.add_submenu_with_toggles::<Delay>( misc_tab.add_submenu_with_toggles::<Delay>(
"Input Delay", "Input Delay",
"input_delay", "input_delay",
"Input Delay: Frames to delay player inputs by", "Input Delay: Frames to delay player inputs by",
true, true,
&(MENU.input_delay.bits as u32),
); );
misc_tab.add_submenu_with_toggles::<OnOff>( misc_tab.add_submenu_with_toggles::<OnOff>(
"Stage Hazards", "Stage Hazards",
"stage_hazards", "stage_hazards",
"Stage Hazards: Should stage hazards be present", "Stage Hazards: Should stage hazards be present",
true, true,
&(MENU.stage_hazards as u32),
); );
misc_tab.add_submenu_with_toggles::<OnOff>( misc_tab.add_submenu_with_toggles::<OnOff>(
"Quick Menu", "Quick Menu",
"quick_menu", "quick_menu",
"Quick Menu: Should use quick or web menu", "Quick Menu: Should use quick or web menu",
true, true,
&(MENU.quick_menu as u32),
); );
overall_menu.tabs.push(misc_tab); overall_menu.tabs.push(misc_tab);
let non_ui_menu = serde_json::to_string(&MENU)
.unwrap()
.replace("\"", "")
.replace("{", "")
.replace("}", "");
let toggle_values_all = non_ui_menu.split(',').collect::<Vec<&str>>();
let mut sub_menu_id_to_vals: HashMap<&str, u32> = HashMap::new();
for toggle_values in toggle_values_all {
let toggle_value_split = toggle_values.split(':').collect::<Vec<&str>>();
let sub_menu_id = toggle_value_split[0];
if sub_menu_id.is_empty() {
continue;
}
let full_bits: u32 = toggle_value_split[1].parse().unwrap_or(0);
sub_menu_id_to_vals.insert(&sub_menu_id, full_bits);
}
overall_menu.tabs.iter_mut().for_each(|tab| {
tab.tab_submenus.iter_mut().for_each(|sub_menu| {
let sub_menu_id = sub_menu.submenu_id;
sub_menu.toggles.iter_mut().for_each(|toggle| {
if sub_menu_id_to_vals.contains_key(sub_menu_id)
&& (sub_menu_id_to_vals[sub_menu_id] & (toggle.toggle_value as u32) != 0)
{
toggle.checked = true
}
})
})
});
overall_menu overall_menu
} }

View file

@ -0,0 +1,27 @@
pub enum GaugeState {
MinHover,
MaxHover,
MinSelected,
MaxSelected,
None,
}
pub struct DoubleEndedGauge {
pub state: GaugeState,
pub selected_min: u32,
pub selected_max: u32,
pub abs_min: u32,
pub abs_max: u32,
}
impl DoubleEndedGauge {
pub fn new() -> DoubleEndedGauge {
DoubleEndedGauge {
state: GaugeState::None,
selected_min: 0,
selected_max: 150,
abs_min: 0,
abs_max: 150,
}
}
}

View file

@ -1,19 +1,24 @@
use training_mod_consts::{Slider, SubMenu, SubMenuType, Toggle, UiMenu}; use training_mod_consts::{Slider, SubMenu, SubMenuType, Toggle, UiMenu};
use tui::{ use tui::{
backend::{Backend}, backend::Backend,
layout::{Constraint, Corner, Direction, Layout}, layout::{Constraint, Corner, Direction, Layout, Rect},
style::{Modifier, Style}, style::{Modifier, Style},
text::{Span, Spans}, text::{Span, Spans},
widgets::{Tabs, Paragraph, Block, List, ListItem, ListState}, widgets::{Block, LineGauge, List, ListItem, ListState, Paragraph, Tabs},
Frame, Frame,
}; };
pub use tui::{backend::TestBackend, Terminal, style::Color};
use std::collections::HashMap; use std::collections::HashMap;
use serde_json::{Map, json};
pub use tui::{backend::TestBackend, style::Color, Terminal};
mod gauge;
mod list; mod list;
use crate::list::{StatefulList, MultiStatefulList}; use crate::gauge::{DoubleEndedGauge, GaugeState};
use crate::list::{MultiStatefulList, StatefulList};
static NX_TUI_WIDTH: u16 = 66;
/// We should hold a list of SubMenus. /// We should hold a list of SubMenus.
/// The currently selected SubMenu should also have an associated list with necessary information. /// The currently selected SubMenu should also have an associated list with necessary information.
@ -22,8 +27,8 @@ pub struct App<'a> {
pub tabs: StatefulList<&'a str>, pub tabs: StatefulList<&'a str>,
pub menu_items: HashMap<&'a str, MultiStatefulList<SubMenu<'a>>>, pub menu_items: HashMap<&'a str, MultiStatefulList<SubMenu<'a>>>,
pub selected_sub_menu_toggles: MultiStatefulList<Toggle<'a>>, pub selected_sub_menu_toggles: MultiStatefulList<Toggle<'a>>,
pub selected_sub_menu_sliders: MultiStatefulList<Slider>, pub selected_sub_menu_slider: DoubleEndedGauge,
pub outer_list: bool pub outer_list: bool,
} }
impl<'a> App<'a> { impl<'a> App<'a> {
@ -34,7 +39,7 @@ impl<'a> App<'a> {
menu.tabs.iter().for_each(|tab| { menu.tabs.iter().for_each(|tab| {
menu_items_stateful.insert( menu_items_stateful.insert(
tab.tab_title, tab.tab_title,
MultiStatefulList::with_items(tab.tab_submenus.clone(), num_lists) MultiStatefulList::with_items(tab.tab_submenus.clone(), num_lists),
); );
}); });
@ -42,121 +47,286 @@ impl<'a> App<'a> {
tabs: StatefulList::with_items(menu.tabs.iter().map(|tab| tab.tab_title).collect()), tabs: StatefulList::with_items(menu.tabs.iter().map(|tab| tab.tab_title).collect()),
menu_items: menu_items_stateful, menu_items: menu_items_stateful,
selected_sub_menu_toggles: MultiStatefulList::with_items(vec![], 0), selected_sub_menu_toggles: MultiStatefulList::with_items(vec![], 0),
selected_sub_menu_sliders: MultiStatefulList::with_items(vec![], 0), selected_sub_menu_slider: DoubleEndedGauge::new(),
outer_list: true outer_list: true,
}; };
app.set_sub_menu_items(); app.set_sub_menu_items();
app app
} }
/// Takes the currently selected tab/submenu and clones the options into
/// self.selected_sub_menu_toggles and self.selected_sub_menu_slider
pub fn set_sub_menu_items(&mut self) { pub fn set_sub_menu_items(&mut self) {
let (list_section, list_idx) = self.menu_items.get(self.tab_selected()).unwrap().idx_to_list_idx(self.menu_items.get(self.tab_selected()).unwrap().state); let (list_section, list_idx) = self
let selected_sub_menu = &self.menu_items.get(self.tab_selected()).unwrap().lists[list_section].items.get(list_idx).unwrap(); .menu_items
.get(self.tab_selected())
.unwrap()
.idx_to_list_idx(self.menu_items.get(self.tab_selected()).unwrap().state);
let selected_sub_menu = &self.menu_items.get(self.tab_selected()).unwrap().lists
[list_section]
.items
.get(list_idx)
.unwrap();
let toggles = selected_sub_menu.toggles.clone(); let toggles = selected_sub_menu.toggles.clone();
// let sliders = selected_sub_menu.sliders.clone(); let slider = selected_sub_menu.slider.clone();
match SubMenuType::from_str(self.sub_menu_selected()._type) { match SubMenuType::from_str(self.sub_menu_selected()._type) {
SubMenuType::TOGGLE => { SubMenuType::TOGGLE => {
self.selected_sub_menu_toggles = MultiStatefulList::with_items( self.selected_sub_menu_toggles = MultiStatefulList::with_items(
toggles, toggles,
if selected_sub_menu.toggles.len() >= 3 { 3 } else { selected_sub_menu.toggles.len()} ) if selected_sub_menu.toggles.len() >= 3 {
}, 3
} else {
selected_sub_menu.toggles.len()
},
)
}
SubMenuType::SLIDER => { SubMenuType::SLIDER => {
// self.selected_sub_menu_sliders = MultiStatefulList::with_items( let slider = slider.unwrap();
// sliders, self.selected_sub_menu_slider = DoubleEndedGauge {
// if selected_sub_menu.sliders.len() >= 3 { 3 } else { selected_sub_menu.sliders.len()} ) state: GaugeState::None,
}, selected_min: slider.selected_min,
selected_max: slider.selected_max,
abs_min: slider.abs_min,
abs_max: slider.abs_max,
}
}
}; };
} }
/// Returns the id of the currently selected tab
fn tab_selected(&self) -> &str { fn tab_selected(&self) -> &str {
self.tabs.items.get(self.tabs.state.selected().unwrap()).unwrap() self.tabs
.items
.get(self.tabs.state.selected().unwrap())
.unwrap()
} }
/// Returns the currently selected SubMenu struct
///
/// {
/// submenu_title: &'a str,
/// submenu_id: &'a str,
/// help_text: &'a str,
/// is_single_option: bool,
/// toggles: Vec<Toggle<'a>>,
/// slider: Option<Slider>,
/// _type: &'a str,
/// }
fn sub_menu_selected(&self) -> &SubMenu { fn sub_menu_selected(&self) -> &SubMenu {
let (list_section, list_idx) = self.menu_items.get(self.tab_selected()).unwrap().idx_to_list_idx(self.menu_items.get(self.tab_selected()).unwrap().state); let (list_section, list_idx) = self
self.menu_items.get(self.tab_selected()).unwrap().lists[list_section].items.get(list_idx).unwrap() .menu_items
.get(self.tab_selected())
.unwrap()
.idx_to_list_idx(self.menu_items.get(self.tab_selected()).unwrap().state);
self.menu_items.get(self.tab_selected()).unwrap().lists[list_section]
.items
.get(list_idx)
.unwrap()
} }
/// A "next()" function which differs per submenu type
/// Toggles: calls next()
/// Slider: Swaps between MinHover and MaxHover
pub fn sub_menu_next(&mut self) { pub fn sub_menu_next(&mut self) {
match SubMenuType::from_str(self.sub_menu_selected()._type) { match SubMenuType::from_str(self.sub_menu_selected()._type) {
SubMenuType::TOGGLE => self.selected_sub_menu_toggles.next(), SubMenuType::TOGGLE => self.selected_sub_menu_toggles.next(),
SubMenuType::SLIDER => self.selected_sub_menu_sliders.next(), SubMenuType::SLIDER => match self.selected_sub_menu_slider.state {
GaugeState::MinHover => self.selected_sub_menu_slider.state = GaugeState::MaxHover,
GaugeState::MaxHover => self.selected_sub_menu_slider.state = GaugeState::MinHover,
_ => {}
},
} }
} }
/// A "next_list()" function which differs per submenu type
/// Toggles: Calls next_list()
/// Slider:
/// * Swaps between MinHover and MaxHover
/// * Increments the selected_min/max if possible
pub fn sub_menu_next_list(&mut self) { pub fn sub_menu_next_list(&mut self) {
match SubMenuType::from_str(self.sub_menu_selected()._type) { match SubMenuType::from_str(self.sub_menu_selected()._type) {
SubMenuType::TOGGLE => self.selected_sub_menu_toggles.next_list(), SubMenuType::TOGGLE => self.selected_sub_menu_toggles.next_list(),
SubMenuType::SLIDER => self.selected_sub_menu_sliders.next_list(), SubMenuType::SLIDER => match self.selected_sub_menu_slider.state {
GaugeState::MinHover => self.selected_sub_menu_slider.state = GaugeState::MaxHover,
GaugeState::MaxHover => self.selected_sub_menu_slider.state = GaugeState::MinHover,
GaugeState::MinSelected => {
if self.selected_sub_menu_slider.selected_min
< self.selected_sub_menu_slider.selected_max
{
self.selected_sub_menu_slider.selected_min += 1;
}
}
GaugeState::MaxSelected => {
if self.selected_sub_menu_slider.selected_max
< self.selected_sub_menu_slider.abs_max
{
self.selected_sub_menu_slider.selected_max += 1;
}
}
GaugeState::None => {}
},
} }
} }
/// A "previous()" function which differs per submenu type
/// Toggles: calls previous()
/// Slider: Swaps between MinHover and MaxHover
pub fn sub_menu_previous(&mut self) { pub fn sub_menu_previous(&mut self) {
match SubMenuType::from_str(self.sub_menu_selected()._type) { match SubMenuType::from_str(self.sub_menu_selected()._type) {
SubMenuType::TOGGLE => self.selected_sub_menu_toggles.previous(), SubMenuType::TOGGLE => self.selected_sub_menu_toggles.previous(),
SubMenuType::SLIDER => self.selected_sub_menu_sliders.previous(), SubMenuType::SLIDER => match self.selected_sub_menu_slider.state {
GaugeState::MinHover => self.selected_sub_menu_slider.state = GaugeState::MaxHover,
GaugeState::MaxHover => self.selected_sub_menu_slider.state = GaugeState::MinHover,
_ => {}
},
} }
} }
/// A "previous_list()" function which differs per submenu type
/// Toggles: Calls previous_list()
/// Slider:
/// * Swaps between MinHover and MaxHover
/// * Decrements the selected_min/max if possible
pub fn sub_menu_previous_list(&mut self) { pub fn sub_menu_previous_list(&mut self) {
match SubMenuType::from_str(self.sub_menu_selected()._type) { match SubMenuType::from_str(self.sub_menu_selected()._type) {
SubMenuType::TOGGLE => self.selected_sub_menu_toggles.previous_list(), SubMenuType::TOGGLE => self.selected_sub_menu_toggles.previous_list(),
SubMenuType::SLIDER => self.selected_sub_menu_sliders.previous_list(), SubMenuType::SLIDER => match self.selected_sub_menu_slider.state {
GaugeState::MinHover => self.selected_sub_menu_slider.state = GaugeState::MaxHover,
GaugeState::MaxHover => self.selected_sub_menu_slider.state = GaugeState::MinHover,
GaugeState::MinSelected => {
if self.selected_sub_menu_slider.selected_min
> self.selected_sub_menu_slider.abs_min
{
self.selected_sub_menu_slider.selected_min -= 1;
}
}
GaugeState::MaxSelected => {
if self.selected_sub_menu_slider.selected_max
> self.selected_sub_menu_slider.selected_min
{
self.selected_sub_menu_slider.selected_max -= 1;
}
}
GaugeState::None => {}
},
} }
} }
pub fn sub_menu_strs_and_states(&mut self) -> (&str, &str, Vec<(Vec<(bool, &str)>, ListState)>) { /// Returns information about the currently selected submenu
(self.sub_menu_selected().submenu_title, self.sub_menu_selected().help_text, ///
match SubMenuType::from_str(self.sub_menu_selected()._type) { /// 0: Submenu Title
SubMenuType::TOGGLE => { /// 1: Submenu Help Text
self.selected_sub_menu_toggles.lists.iter().map(|toggle_list| { /// 2: Vec(toggle checked, title) for toggles, Vec(nothing) for slider
(toggle_list.items.iter().map( /// 3: ListState for toggles, ListState::new() for slider
|toggle| (toggle.checked, toggle.toggle_title) /// TODO: Refactor return type into a nice struct
).collect(), toggle_list.state.clone()) pub fn sub_menu_strs_and_states(
}).collect() &mut self,
) -> (&str, &str, Vec<(Vec<(bool, &str)>, ListState)>) {
(
self.sub_menu_selected().submenu_title,
self.sub_menu_selected().help_text,
match SubMenuType::from_str(self.sub_menu_selected()._type) {
SubMenuType::TOGGLE => self
.selected_sub_menu_toggles
.lists
.iter()
.map(|toggle_list| {
(
toggle_list
.items
.iter()
.map(|toggle| (toggle.checked, toggle.toggle_title))
.collect(),
toggle_list.state.clone(),
)
})
.collect(),
SubMenuType::SLIDER => {
vec![(vec![], ListState::default())]
}
}, },
SubMenuType::SLIDER => { )
vec![(vec![], ListState::default())]
},
})
} }
/// Returns information about the currently selected slider
/// 0: Title
/// 1: Help text
/// 2: Reference to self.selected_sub_menu_slider
/// TODO: Refactor return type into a nice struct
pub fn sub_menu_strs_for_slider(&mut self) -> (&str, &str, &DoubleEndedGauge) {
let slider = match SubMenuType::from_str(self.sub_menu_selected()._type) {
SubMenuType::SLIDER => &self.selected_sub_menu_slider,
_ => {
panic!("Slider not selected!");
}
};
(
self.sub_menu_selected().submenu_title,
self.sub_menu_selected().help_text,
slider,
)
}
/// Different behavior depending on the current menu location
/// Outer list: Sets self.outer_list to false
/// 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) { pub fn on_a(&mut self) {
let tab_selected = self
.tabs
.items
.get(self.tabs.state.selected().unwrap())
.unwrap();
let (list_section, list_idx) = self
.menu_items
.get(tab_selected)
.unwrap()
.idx_to_list_idx(self.menu_items.get(tab_selected).unwrap().state);
let selected_sub_menu = self.menu_items.get_mut(tab_selected).unwrap().lists
[list_section]
.items
.get_mut(list_idx)
.unwrap();
if self.outer_list { if self.outer_list {
self.outer_list = false; self.outer_list = false;
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.selected_sub_menu_slider.state = GaugeState::MinHover;
}
_ => {}
}
} else { } else {
let tab_selected = self.tabs.items.get(self.tabs.state.selected().unwrap()).unwrap();
let (list_section, list_idx) = self.menu_items.get(tab_selected)
.unwrap()
.idx_to_list_idx(self.menu_items.get(tab_selected).unwrap().state);
let selected_sub_menu = self.menu_items.get_mut(tab_selected)
.unwrap()
.lists[list_section]
.items.get_mut(list_idx).unwrap();
match SubMenuType::from_str(selected_sub_menu._type) { match SubMenuType::from_str(selected_sub_menu._type) {
SubMenuType::TOGGLE => { SubMenuType::TOGGLE => {
let is_single_option = selected_sub_menu.is_single_option; let is_single_option = selected_sub_menu.is_single_option;
let state = self.selected_sub_menu_toggles.state; let state = self.selected_sub_menu_toggles.state;
self.selected_sub_menu_toggles.lists.iter_mut() // Change the toggles in self.selected_sub_menu_toggles (for display)
self.selected_sub_menu_toggles
.lists
.iter_mut()
.map(|list| (list.state.selected(), &mut list.items)) .map(|list| (list.state.selected(), &mut list.items))
.for_each(|(state, toggle_list)| toggle_list.iter_mut() .for_each(|(state, toggle_list)| {
.enumerate() toggle_list.iter_mut().enumerate().for_each(|(i, o)| {
.for_each(|(i, o)| if state.is_some() && i == state.unwrap() {
if state.is_some() && i == state.unwrap() { if !o.checked {
if !o.checked { o.checked = true;
o.checked = true; } else {
} else { o.checked = false;
o.checked = false; }
} } else if is_single_option {
} else if is_single_option { o.checked = false;
o.checked = false; }
} })
)); });
selected_sub_menu.toggles.iter_mut() // Actually change the toggle values in the SubMenu struct
selected_sub_menu
.toggles
.iter_mut()
.enumerate() .enumerate()
.for_each(|(i, o)| { .for_each(|(i, o)| {
if i == state { if i == state {
if !o.checked { if !o.checked {
o.checked = true; o.checked = true;
} else { } else {
@ -166,16 +336,89 @@ impl<'a> App<'a> {
o.checked = false; o.checked = false;
} }
}); });
},
SubMenuType::SLIDER => {
// self.selected_sub_menu_sliders.selected_list_item().checked = true;
} }
SubMenuType::SLIDER => match self.selected_sub_menu_slider.state {
GaugeState::MinHover => {
self.selected_sub_menu_slider.state = GaugeState::MinSelected;
}
GaugeState::MaxHover => {
self.selected_sub_menu_slider.state = GaugeState::MaxSelected;
}
GaugeState::MinSelected => {
self.selected_sub_menu_slider.state = GaugeState::MinHover;
selected_sub_menu.slider = Some(Slider{
selected_min: self.selected_sub_menu_slider.selected_min,
selected_max: self.selected_sub_menu_slider.selected_max,
abs_min: self.selected_sub_menu_slider.abs_min,
abs_max: self.selected_sub_menu_slider.abs_max,
});
}
GaugeState::MaxSelected => {
self.selected_sub_menu_slider.state = GaugeState::MaxHover;
selected_sub_menu.slider = Some(Slider{
selected_min: self.selected_sub_menu_slider.selected_min,
selected_max: self.selected_sub_menu_slider.selected_max,
abs_min: self.selected_sub_menu_slider.abs_min,
abs_max: self.selected_sub_menu_slider.abs_max,
});
}
GaugeState::None => {
self.selected_sub_menu_slider.state = GaugeState::MinHover;
}
},
} }
} }
} }
/// Different behavior depending on the current menu location
/// 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
pub fn on_b(&mut self) { pub fn on_b(&mut self) {
let tab_selected = self
.tabs
.items
.get(self.tabs.state.selected().unwrap())
.unwrap();
let (list_section, list_idx) = self
.menu_items
.get(tab_selected)
.unwrap()
.idx_to_list_idx(self.menu_items.get(tab_selected).unwrap().state);
let selected_sub_menu = self.menu_items.get_mut(tab_selected).unwrap().lists[list_section]
.items
.get_mut(list_idx)
.unwrap();
match SubMenuType::from_str(selected_sub_menu._type) {
SubMenuType::SLIDER => match self.selected_sub_menu_slider.state {
GaugeState::MinSelected => {
self.selected_sub_menu_slider.state = GaugeState::MinHover;
selected_sub_menu.slider = Some(Slider{
selected_min: self.selected_sub_menu_slider.selected_min,
selected_max: self.selected_sub_menu_slider.selected_max,
abs_min: self.selected_sub_menu_slider.abs_min,
abs_max: self.selected_sub_menu_slider.abs_max,
});
// Don't go back to the outer list
return;
}
GaugeState::MaxSelected => {
self.selected_sub_menu_slider.state = GaugeState::MaxHover;
selected_sub_menu.slider = Some(Slider{
selected_min: self.selected_sub_menu_slider.selected_min,
selected_max: self.selected_sub_menu_slider.selected_max,
abs_min: self.selected_sub_menu_slider.abs_min,
abs_max: self.selected_sub_menu_slider.abs_max,
});
// Don't go back to the outer list
return;
}
_ => {}
},
_ => {}
}
self.outer_list = true; self.outer_list = true;
self.set_sub_menu_items();
} }
pub fn on_l(&mut self) { pub fn on_l(&mut self) {
@ -194,7 +437,15 @@ impl<'a> App<'a> {
pub fn on_up(&mut self) { pub fn on_up(&mut self) {
if self.outer_list { if self.outer_list {
self.menu_items.get_mut(self.tabs.items.get(self.tabs.state.selected().unwrap()).unwrap()).unwrap().previous(); self.menu_items
.get_mut(
self.tabs
.items
.get(self.tabs.state.selected().unwrap())
.unwrap(),
)
.unwrap()
.previous();
self.set_sub_menu_items(); self.set_sub_menu_items();
} else { } else {
self.sub_menu_previous(); self.sub_menu_previous();
@ -203,7 +454,15 @@ impl<'a> App<'a> {
pub fn on_down(&mut self) { pub fn on_down(&mut self) {
if self.outer_list { if self.outer_list {
self.menu_items.get_mut(self.tabs.items.get(self.tabs.state.selected().unwrap()).unwrap()).unwrap().next(); self.menu_items
.get_mut(
self.tabs
.items
.get(self.tabs.state.selected().unwrap())
.unwrap(),
)
.unwrap()
.next();
self.set_sub_menu_items(); self.set_sub_menu_items();
} else { } else {
self.sub_menu_next(); self.sub_menu_next();
@ -212,7 +471,15 @@ impl<'a> App<'a> {
pub fn on_left(&mut self) { pub fn on_left(&mut self) {
if self.outer_list { if self.outer_list {
self.menu_items.get_mut(self.tabs.items.get(self.tabs.state.selected().unwrap()).unwrap()).unwrap().previous_list(); self.menu_items
.get_mut(
self.tabs
.items
.get(self.tabs.state.selected().unwrap())
.unwrap(),
)
.unwrap()
.previous_list();
self.set_sub_menu_items(); self.set_sub_menu_items();
} else { } else {
self.sub_menu_previous_list(); self.sub_menu_previous_list();
@ -221,7 +488,15 @@ impl<'a> App<'a> {
pub fn on_right(&mut self) { pub fn on_right(&mut self) {
if self.outer_list { if self.outer_list {
self.menu_items.get_mut(self.tabs.items.get(self.tabs.state.selected().unwrap()).unwrap()).unwrap().next_list(); self.menu_items
.get_mut(
self.tabs
.items
.get(self.tabs.state.selected().unwrap())
.unwrap(),
)
.unwrap()
.next_list();
self.set_sub_menu_items(); self.set_sub_menu_items();
} else { } else {
self.sub_menu_next_list(); self.sub_menu_next_list();
@ -232,36 +507,106 @@ impl<'a> App<'a> {
pub fn ui<B: Backend>(f: &mut Frame<B>, app: &mut App) -> String { pub fn ui<B: Backend>(f: &mut Frame<B>, app: &mut App) -> String {
let app_tabs = &app.tabs; let app_tabs = &app.tabs;
let tab_selected = app_tabs.state.selected().unwrap(); let tab_selected = app_tabs.state.selected().unwrap();
let titles = app_tabs.items.iter().cloned().enumerate().map(|(idx, tab)|{ let mut span_selected = Spans::default();
if idx == tab_selected {
Spans::from(">> ".to_owned() + tab) let titles: Vec<Spans> = app_tabs
} else { .items
Spans::from(" ".to_owned() + tab) .iter()
} .cloned()
}).collect(); .enumerate()
.map(|(idx, tab)| {
if idx == tab_selected {
span_selected = Spans::from("> ".to_owned() + tab);
Spans::from("> ".to_owned() + tab)
} else {
Spans::from(" ".to_owned() + tab)
}
})
.collect();
// There is only enough room to display 3 tabs of text
// So lets replace tabs not near the selected with "..."
let all_windows: Vec<&[Spans]> = titles
.windows(3)
.filter(|w| w.contains(&titles[tab_selected]))
.collect();
let first_window = all_windows[0];
let mut titles: Vec<Spans> = titles
.iter()
.cloned()
.map(
// Converts all tabs not in the window to "..."
|t| {
if first_window.contains(&t) {
t
} else {
Spans::from("...".to_owned())
}
},
)
.collect();
// Don't keep consecutive "..." tabs
titles.dedup();
// Now that the size of the titles vector has changed, need to re-locate the selected tab
let tab_selected_deduped: usize = titles
.iter()
.cloned()
.position(|span| span == span_selected)
.unwrap_or(0);
let tabs = Tabs::new(titles) let tabs = Tabs::new(titles)
.block(Block::default() .block(Block::default().title(Spans::from(Span::styled(
.title( "Ultimate Training Modpack Menu",
Spans::from( Style::default().fg(Color::LightRed),
Span::styled("Ultimate Training Modpack Menu", ))))
Style::default().fg(Color::LightRed)))))
.style(Style::default().fg(Color::White)) .style(Style::default().fg(Color::White))
.highlight_style(Style::default().fg(Color::Yellow)) .highlight_style(Style::default().fg(Color::Yellow))
.divider("|") .divider("|")
.select(tab_selected); .select(tab_selected_deduped);
let vertical_chunks = Layout::default() let vertical_chunks = Layout::default()
.direction(Direction::Vertical) .direction(Direction::Vertical)
.constraints([ .constraints(
Constraint::Length(2), [
Constraint::Max(10), Constraint::Length(2),
Constraint::Length(2)].as_ref()) Constraint::Max(10),
Constraint::Length(2),
]
.as_ref(),
)
.split(f.size()); .split(f.size());
// Prevent overflow by adding a length constraint of NX_TUI_WIDTH
// Need to add a second constraint since the .expand_to_fill() method
// is not publicly exposed, and the attribute defaults to true.
// https://github.com/fdehau/tui-rs/blob/v0.19.0/src/layout.rs#L121
let vertical_chunks: Vec<Rect> = vertical_chunks
.iter()
.map(|chunk| {
Layout::default()
.direction(Direction::Horizontal)
.constraints(
[
Constraint::Length(NX_TUI_WIDTH), // Width of the TUI terminal
Constraint::Min(0), // Fill the remainder margin
]
.as_ref(),
)
.split(*chunk)[0]
}
)
.collect();
let list_chunks = Layout::default() let list_chunks = Layout::default()
.direction(Direction::Horizontal) .direction(Direction::Horizontal)
.constraints([Constraint::Percentage(33), Constraint::Percentage(32), Constraint::Percentage(33)].as_ref()) .constraints(
[
Constraint::Percentage(33),
Constraint::Percentage(33),
Constraint::Percentage(33),
]
.as_ref(),
)
.split(vertical_chunks[1]); .split(vertical_chunks[1]);
f.render_widget(tabs, vertical_chunks[0]); f.render_widget(tabs, vertical_chunks[0]);
@ -269,25 +614,33 @@ pub fn ui<B: Backend>(f: &mut Frame<B>, app: &mut App) -> String {
if app.outer_list { if app.outer_list {
let tab_selected = app.tab_selected(); let tab_selected = app.tab_selected();
let mut item_help = None; let mut item_help = None;
for (list_section, stateful_list) in app.menu_items.get(tab_selected).unwrap().lists.iter().enumerate() { for (list_section, stateful_list) in app
.menu_items
.get(tab_selected)
.unwrap()
.lists
.iter()
.enumerate()
{
let items: Vec<ListItem> = stateful_list let items: Vec<ListItem> = stateful_list
.items .items
.iter() .iter()
.map(|i| { .map(|i| {
let lines = vec![Spans::from( let lines = vec![Spans::from(if stateful_list.state.selected().is_some() {
if stateful_list.state.selected().is_some() { i.submenu_title.to_owned()
i.submenu_title.to_owned() } else {
} else { " ".to_owned() + i.submenu_title
" ".to_owned() + i.submenu_title })];
})];
ListItem::new(lines).style(Style::default().fg(Color::White)) ListItem::new(lines).style(Style::default().fg(Color::White))
}) })
.collect(); .collect();
let list = List::new(items) let list = List::new(items)
.block(Block::default() .block(
.title(if list_section == 0 { "Options" } else { "" }) Block::default()
.style(Style::default().fg(Color::LightRed))) .title(if list_section == 0 { "Options" } else { "" })
.style(Style::default().fg(Color::LightRed)),
)
.highlight_style( .highlight_style(
Style::default() Style::default()
.fg(Color::Green) .fg(Color::Green)
@ -305,63 +658,151 @@ pub fn ui<B: Backend>(f: &mut Frame<B>, app: &mut App) -> String {
// TODO: Add Save Defaults // TODO: Add Save Defaults
let help_paragraph = Paragraph::new( let help_paragraph = Paragraph::new(
item_help.unwrap_or("").replace('\"', "") + item_help.unwrap_or("").replace('\"', "")
"\nA: Enter sub-menu | B: Exit menu | ZL/ZR: Next tab" + "\nA: Enter sub-menu | B: Exit menu | ZL/ZR: Next tab",
).style(Style::default().fg(Color::Cyan)); )
.style(Style::default().fg(Color::Cyan));
f.render_widget(help_paragraph, vertical_chunks[2]); f.render_widget(help_paragraph, vertical_chunks[2]);
} else { } else {
let (title, help_text, mut sub_menu_str_lists) = app.sub_menu_strs_and_states(); if matches!(app.selected_sub_menu_slider.state, GaugeState::None) {
for list_section in 0..sub_menu_str_lists.len() { let (title, help_text, mut sub_menu_str_lists) = app.sub_menu_strs_and_states();
let sub_menu_str = sub_menu_str_lists[list_section].0.clone(); for list_section in 0..sub_menu_str_lists.len() {
let sub_menu_state = &mut sub_menu_str_lists[list_section].1; let sub_menu_str = sub_menu_str_lists[list_section].0.clone();
let values_items: Vec<ListItem> = sub_menu_str.iter().map(|s| { let sub_menu_state = &mut sub_menu_str_lists[list_section].1;
ListItem::new( let values_items: Vec<ListItem> = sub_menu_str
vec![ .iter()
Spans::from((if s.0 { "X " } else { " " }).to_owned() + s.1) .map(|s| {
] ListItem::new(vec![Spans::from(
) (if s.0 { "X " } else { " " }).to_owned() + s.1,
}).collect(); )])
})
.collect();
let values_list = List::new(values_items) let values_list = List::new(values_items)
.block(Block::default().title(if list_section == 0 { title } else { "" })) .block(Block::default().title(if list_section == 0 { title } else { "" }))
.start_corner(Corner::TopLeft) .start_corner(Corner::TopLeft)
.highlight_style( .highlight_style(
Style::default() Style::default()
.fg(Color::LightGreen) .fg(Color::LightGreen)
.add_modifier(Modifier::BOLD), .add_modifier(Modifier::BOLD),
) )
.highlight_symbol(">> "); .highlight_symbol(">> ");
f.render_stateful_widget(values_list, list_chunks[list_section], sub_menu_state); f.render_stateful_widget(values_list, list_chunks[list_section], sub_menu_state);
}
let help_paragraph = Paragraph::new(
help_text.replace('\"', "") + "\nA: Select toggle | B: Exit submenu",
)
.style(Style::default().fg(Color::Cyan));
f.render_widget(help_paragraph, vertical_chunks[2]);
} else {
let (_title, help_text, gauge_vals) = app.sub_menu_strs_for_slider();
let abs_min = gauge_vals.abs_min;
let abs_max = gauge_vals.abs_max;
let selected_min = gauge_vals.selected_min;
let selected_max = gauge_vals.selected_max;
let lbl_ratio = 0.95; // Needed so that the upper limit label is visible
let constraints = [
Constraint::Ratio((lbl_ratio * (selected_min-abs_min) as f32) as u32, abs_max-abs_min),
Constraint::Ratio((lbl_ratio * (selected_max-selected_min) as f32) as u32, abs_max-abs_min),
Constraint::Ratio((lbl_ratio * (abs_max-selected_max) as f32) as u32, abs_max-abs_min),
Constraint::Min(3), // For upper limit label
];
let gauge_chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints(constraints)
.split(vertical_chunks[1]);
let slider_lbls = [
abs_min,
selected_min,
selected_max,
abs_max,
];
for (idx, lbl) in slider_lbls.iter().enumerate() {
let mut line_set = tui::symbols::line::NORMAL;
line_set.horizontal = "-";
let mut gauge = LineGauge::default()
.ratio(1.0)
.label(format!("{}", lbl))
.style(Style::default().fg(Color::White))
.line_set(line_set)
.gauge_style(Style::default().fg(Color::White).bg(Color::Black));
if idx == 1 {
// Slider between selected_min and selected_max
match gauge_vals.state {
GaugeState::MinHover => {
gauge = gauge.style(Style::default().fg(Color::Red))
}
GaugeState::MinSelected => {
gauge = gauge.style(Style::default().fg(Color::Green))
}
_ => {}
}
gauge = gauge.gauge_style(Style::default().fg(Color::Yellow).bg(Color::Black));
} else if idx == 2 {
// Slider between selected_max and abs_max
match gauge_vals.state {
GaugeState::MaxHover => {
gauge = gauge.style(Style::default().fg(Color::Red))
}
GaugeState::MaxSelected => {
gauge = gauge.style(Style::default().fg(Color::Green))
}
_ => {}
}
} else if idx == 3 {
// Slider for abs_max
// We only want the label to show, so set the line character to " "
let mut line_set = tui::symbols::line::NORMAL;
line_set.horizontal = " ";
gauge = gauge.line_set(line_set);
// For some reason, the selected_max slider displays on top
// So we need to change the abs_max slider styling to match
// If the selected_max is close enough to the abs_max
if (selected_max as f32 / abs_max as f32) > 0.95 {
gauge = gauge.style(match gauge_vals.state {
GaugeState::MaxHover => Style::default().fg(Color::Red),
GaugeState::MaxSelected => Style::default().fg(Color::Green),
_ => Style::default(),
})
}
}
f.render_widget(gauge, gauge_chunks[idx]);
}
let help_paragraph = Paragraph::new(
help_text.replace('\"', "") + "\nA: Select toggle | B: Exit submenu",
)
.style(Style::default().fg(Color::Cyan));
f.render_widget(help_paragraph, vertical_chunks[2]);
} }
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]);
} }
let mut settings = HashMap::new(); // Collect settings
let mut settings = Map::new();
// Collect settings for toggles
for key in app.menu_items.keys() { for key in app.menu_items.keys() {
for list in &app.menu_items.get(key).unwrap().lists { for list in &app.menu_items.get(key).unwrap().lists {
for sub_menu in &list.items { for sub_menu in &list.items {
let val : usize = sub_menu.toggles.iter() if !sub_menu.toggles.is_empty() {
.filter(|t| t.checked) let val: u32 = sub_menu
.map(|t| t.toggle_value) .toggles
.sum(); .iter()
.filter(|t| t.checked)
settings.insert(sub_menu.submenu_id, val); .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() serde_json::to_string(&settings).unwrap()
// TODO: Add saveDefaults // TODO: Add saveDefaults
// if (document.getElementById("saveDefaults").checked) {
// url += "save_defaults=1";
// } else {
// url = url.slice(0, -1);
// }
} }

View file

@ -21,7 +21,6 @@ impl<T: Clone> MultiStatefulList<T> {
(self.total_len as f32 / self.lists.len() as f32).ceil() as usize * (list_section + 1), (self.total_len as f32 / self.lists.len() as f32).ceil() as usize * (list_section + 1),
self.total_len); self.total_len);
if (list_section_min_idx..list_section_max_idx).contains(&idx) { if (list_section_min_idx..list_section_max_idx).contains(&idx) {
// println!("\n{}: ({}, {})", idx, list_section_min_idx, list_section_max_idx);
return (list_section, idx - list_section_min_idx) return (list_section, idx - list_section_min_idx)
} }
} }

View file

@ -54,6 +54,8 @@ fn ensure_menu_retains_selections() -> Result<(), Box<dyn Error>> {
} }
fn main() -> Result<(), Box<dyn Error>> { fn main() -> Result<(), Box<dyn Error>> {
let args: Vec<String> = std::env::args().collect();
let inputs = args.get(1);
let menu; let menu;
unsafe { unsafe {
menu = get_menu(); menu = get_menu();
@ -61,6 +63,21 @@ fn main() -> Result<(), Box<dyn Error>> {
#[cfg(not(feature = "has_terminal"))] { #[cfg(not(feature = "has_terminal"))] {
let (mut terminal, mut app) = test_backend_setup(menu)?; let (mut terminal, mut app) = test_backend_setup(menu)?;
if inputs.is_some() {
inputs.unwrap().split(",").for_each(|input| {
match input.to_uppercase().as_str() {
"L" => app.on_l(),
"R" => app.on_r(),
"A" => app.on_a(),
"B" => app.on_b(),
"UP" => app.on_up(),
"DOWN" => app.on_down(),
"LEFT" => app.on_left(),
"RIGHT" => app.on_right(),
_ => {}
}
})
}
let mut json_response = String::new(); 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| json_response = training_mod_tui::ui(f, &mut app))?;