mirror of
https://github.com/jugeeya/UltimateTrainingModpack.git
synced 2026-01-22 02:10:24 +00:00
Quick Menu + Ryujinx Compatibility (#313)
* Initial commit * Format Rust code using rustfmt * Add back fs calls * working with drawing * wow we're almost there * multi-lists working, selection within tui working * working with tabs * working with smash * amend warnings, fix menu actually saving inputs * small refactors * Fully working! * Fix warnings Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
This commit is contained in:
18
training_mod_tui/Cargo.toml
Normal file
18
training_mod_tui/Cargo.toml
Normal file
@@ -0,0 +1,18 @@
|
||||
[package]
|
||||
name = "training_mod_tui"
|
||||
version = "0.1.0"
|
||||
edition = "2018"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
tui = { version = "0.16.0", default-features = false }
|
||||
unicode-width = "0.1.9"
|
||||
training_mod_consts = { path = "../training_mod_consts", default-features = false}
|
||||
serde_json = "1.0.79"
|
||||
bitflags = "1.2.1"
|
||||
crossterm = { version = "0.22.1", optional = true }
|
||||
|
||||
[features]
|
||||
default = []
|
||||
has_terminal = ["crossterm", "tui/crossterm"]
|
||||
454
training_mod_tui/src/lib.rs
Normal file
454
training_mod_tui/src/lib.rs
Normal file
@@ -0,0 +1,454 @@
|
||||
use training_mod_consts::{OnOffSelector, Slider, SubMenu, SubMenuType, Toggle};
|
||||
use tui::{
|
||||
backend::{Backend},
|
||||
layout::{Constraint, Corner, Direction, Layout},
|
||||
style::{Color, Modifier, Style},
|
||||
text::Spans,
|
||||
widgets::{Tabs, Paragraph, Block, List, ListItem, ListState},
|
||||
Frame,
|
||||
};
|
||||
|
||||
pub use tui::{backend::TestBackend, Terminal};
|
||||
use std::collections::HashMap;
|
||||
|
||||
mod list;
|
||||
|
||||
use crate::list::{StatefulList, MultiStatefulList};
|
||||
|
||||
/// We should hold a list of SubMenus.
|
||||
/// The currently selected SubMenu should also have an associated list with necessary information.
|
||||
/// We can convert the option types (Toggle, OnOff, Slider) to lists
|
||||
pub struct App<'a> {
|
||||
pub tabs: StatefulList<&'a str>,
|
||||
pub menu_items: HashMap<&'a str, MultiStatefulList<SubMenu<'a>>>,
|
||||
pub selected_sub_menu_toggles: MultiStatefulList<Toggle<'a>>,
|
||||
pub selected_sub_menu_onoff_selectors: MultiStatefulList<OnOffSelector<'a>>,
|
||||
pub selected_sub_menu_sliders: MultiStatefulList<Slider>,
|
||||
pub outer_list: bool
|
||||
}
|
||||
|
||||
impl<'a> App<'a> {
|
||||
pub fn new(menu: training_mod_consts::Menu<'a>) -> App<'a> {
|
||||
let tab_specifiers = vec![
|
||||
("Mash Settings", vec![
|
||||
"Mash Toggles",
|
||||
"Followup Toggles",
|
||||
"Attack Angle",
|
||||
"Ledge Options",
|
||||
"Ledge Delay",
|
||||
"Tech Options",
|
||||
"Miss Tech Options",
|
||||
"Defensive Options",
|
||||
"Aerial Delay",
|
||||
"OoS Offset",
|
||||
"Reaction Time",
|
||||
]),
|
||||
("Defensive Settings", vec![
|
||||
"Fast Fall",
|
||||
"Fast Fall Delay",
|
||||
"Falling Aerials",
|
||||
"Full Hop",
|
||||
"Shield Tilt",
|
||||
"DI Direction",
|
||||
"SDI Direction",
|
||||
"Airdodge Direction",
|
||||
"SDI Strength",
|
||||
"Shield Toggles",
|
||||
"Mirroring",
|
||||
"Throw Options",
|
||||
"Throw Delay",
|
||||
"Pummel Delay",
|
||||
"Buff Options",
|
||||
]),
|
||||
("Other Settings", vec![
|
||||
"Input Delay",
|
||||
"Save States",
|
||||
"Save Damage",
|
||||
"Hitbox Visualization",
|
||||
"Stage Hazards",
|
||||
"Frame Advantage",
|
||||
"Mash In Neutral",
|
||||
"Quick Menu"
|
||||
])
|
||||
];
|
||||
let mut tabs: std::collections::HashMap<&str, Vec<SubMenu>> = std::collections::HashMap::new();
|
||||
tabs.insert("Mash Settings", vec![]);
|
||||
tabs.insert("Defensive Settings", vec![]);
|
||||
tabs.insert("Other Settings", vec![]);
|
||||
|
||||
for sub_menu in menu.sub_menus.iter() {
|
||||
for tab_spec in tab_specifiers.iter() {
|
||||
if tab_spec.1.contains(&sub_menu.title) {
|
||||
tabs.get_mut(tab_spec.0).unwrap().push(sub_menu.clone());
|
||||
}
|
||||
}
|
||||
};
|
||||
let num_lists = 3;
|
||||
|
||||
let mut menu_items_stateful = HashMap::new();
|
||||
tabs.keys().for_each(|k| {
|
||||
menu_items_stateful.insert(
|
||||
k.clone(),
|
||||
MultiStatefulList::with_items(tabs.get(k).unwrap().clone(), num_lists)
|
||||
);
|
||||
});
|
||||
let mut app = App {
|
||||
tabs: StatefulList::with_items(tab_specifiers.iter().map(|(tab_title, _)| *tab_title).collect()),
|
||||
menu_items: menu_items_stateful,
|
||||
selected_sub_menu_toggles: MultiStatefulList::with_items(vec![], 0),
|
||||
selected_sub_menu_onoff_selectors: MultiStatefulList::with_items(vec![], 0),
|
||||
selected_sub_menu_sliders: MultiStatefulList::with_items(vec![], 0),
|
||||
outer_list: true
|
||||
};
|
||||
app.set_sub_menu_items();
|
||||
app
|
||||
}
|
||||
|
||||
pub fn set_sub_menu_items(&mut self) {
|
||||
let (list_section, list_idx) = self.menu_items.get(self.tab_selected()).unwrap().idx_to_list_idx(self.menu_items.get(self.tab_selected()).unwrap().state);
|
||||
let selected_sub_menu = &self.menu_items.get(self.tab_selected()).unwrap().lists[list_section].items.get(list_idx).unwrap();
|
||||
|
||||
let toggles = selected_sub_menu.toggles.clone();
|
||||
let sliders = selected_sub_menu.sliders.clone();
|
||||
let onoffs = selected_sub_menu.onoffselector.clone();
|
||||
match SubMenuType::from_str(self.sub_menu_selected()._type) {
|
||||
SubMenuType::TOGGLE => {
|
||||
self.selected_sub_menu_toggles = MultiStatefulList::with_items(
|
||||
toggles,
|
||||
if selected_sub_menu.toggles.len() >= 3 { 3 } else { selected_sub_menu.toggles.len()} )
|
||||
},
|
||||
SubMenuType::SLIDER => {
|
||||
self.selected_sub_menu_sliders = MultiStatefulList::with_items(
|
||||
sliders,
|
||||
if selected_sub_menu.sliders.len() >= 3 { 3 } else { selected_sub_menu.sliders.len()} )
|
||||
},
|
||||
SubMenuType::ONOFF => {
|
||||
self.selected_sub_menu_onoff_selectors = MultiStatefulList::with_items(
|
||||
onoffs,
|
||||
if selected_sub_menu.onoffselector.len() >= 3 { 3 } else { selected_sub_menu.onoffselector.len()} )
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
fn tab_selected(&self) -> &str {
|
||||
self.tabs.items.get(self.tabs.state.selected().unwrap()).unwrap()
|
||||
}
|
||||
|
||||
fn sub_menu_selected(&self) -> &SubMenu {
|
||||
let (list_section, list_idx) = self.menu_items.get(self.tab_selected()).unwrap().idx_to_list_idx(self.menu_items.get(self.tab_selected()).unwrap().state);
|
||||
&self.menu_items.get(self.tab_selected()).unwrap().lists[list_section].items.get(list_idx).unwrap()
|
||||
}
|
||||
|
||||
pub fn sub_menu_next(&mut self) {
|
||||
match SubMenuType::from_str(self.sub_menu_selected()._type) {
|
||||
SubMenuType::TOGGLE => self.selected_sub_menu_toggles.next(),
|
||||
SubMenuType::SLIDER => self.selected_sub_menu_sliders.next(),
|
||||
SubMenuType::ONOFF => self.selected_sub_menu_onoff_selectors.next(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn sub_menu_next_list(&mut self) {
|
||||
match SubMenuType::from_str(self.sub_menu_selected()._type) {
|
||||
SubMenuType::TOGGLE => self.selected_sub_menu_toggles.next_list(),
|
||||
SubMenuType::SLIDER => self.selected_sub_menu_sliders.next_list(),
|
||||
SubMenuType::ONOFF => self.selected_sub_menu_onoff_selectors.next_list(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn sub_menu_previous(&mut self) {
|
||||
match SubMenuType::from_str(self.sub_menu_selected()._type) {
|
||||
SubMenuType::TOGGLE => self.selected_sub_menu_toggles.previous(),
|
||||
SubMenuType::SLIDER => self.selected_sub_menu_sliders.previous(),
|
||||
SubMenuType::ONOFF => self.selected_sub_menu_onoff_selectors.previous(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn sub_menu_previous_list(&mut self) {
|
||||
match SubMenuType::from_str(self.sub_menu_selected()._type) {
|
||||
SubMenuType::TOGGLE => self.selected_sub_menu_toggles.previous_list(),
|
||||
SubMenuType::SLIDER => self.selected_sub_menu_sliders.previous_list(),
|
||||
SubMenuType::ONOFF => self.selected_sub_menu_onoff_selectors.previous_list(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn sub_menu_strs_and_states(&mut self) -> (&str, &str, Vec<(Vec<(&str, &str)>, ListState)>) {
|
||||
(self.sub_menu_selected().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.title)
|
||||
).collect(), toggle_list.state.clone())
|
||||
}).collect()
|
||||
},
|
||||
SubMenuType::SLIDER => {
|
||||
vec![(vec![], ListState::default())]
|
||||
},
|
||||
SubMenuType::ONOFF => {
|
||||
self.selected_sub_menu_onoff_selectors.lists.iter().map(|onoff_list| {
|
||||
(onoff_list.items.iter().map(
|
||||
|onoff| (onoff.checked, onoff.title)
|
||||
).collect(), onoff_list.state.clone())
|
||||
}).collect()
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
pub fn on_a(&mut self) {
|
||||
if self.outer_list {
|
||||
self.outer_list = false;
|
||||
} 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) {
|
||||
SubMenuType::TOGGLE => {
|
||||
let is_single_option = selected_sub_menu.is_single_option.is_some();
|
||||
let state = self.selected_sub_menu_toggles.state;
|
||||
self.selected_sub_menu_toggles.lists.iter_mut()
|
||||
.map(|list| (list.state.selected(), &mut list.items))
|
||||
.for_each(|(state, toggle_list)| toggle_list.iter_mut()
|
||||
.enumerate()
|
||||
.for_each(|(i, o)|
|
||||
if state.is_some() && i == state.unwrap() {
|
||||
if o.checked != "is-appear" {
|
||||
o.checked = "is-appear";
|
||||
} else {
|
||||
o.checked = "is-hidden";
|
||||
}
|
||||
} else if is_single_option {
|
||||
o.checked = "is-hidden";
|
||||
}
|
||||
));
|
||||
selected_sub_menu.toggles.iter_mut()
|
||||
.enumerate()
|
||||
.for_each(|(i, o)| {
|
||||
if i == state {
|
||||
if o.checked != "is-appear" {
|
||||
o.checked = "is-appear";
|
||||
} else {
|
||||
o.checked = "is-hidden";
|
||||
}
|
||||
} else if is_single_option {
|
||||
o.checked = "is-hidden";
|
||||
}
|
||||
});
|
||||
},
|
||||
SubMenuType::ONOFF => {
|
||||
let onoff = self.selected_sub_menu_onoff_selectors.selected_list_item();
|
||||
if onoff.checked != "is-appear" {
|
||||
onoff.checked = "is-appear";
|
||||
} else {
|
||||
onoff.checked = "is-hidden";
|
||||
}
|
||||
selected_sub_menu.onoffselector.iter_mut()
|
||||
.filter(|o| o.title == onoff.title)
|
||||
.for_each(|o| o.checked = onoff.checked);
|
||||
},
|
||||
SubMenuType::SLIDER => {
|
||||
// self.selected_sub_menu_sliders.selected_list_item().checked = "is-appear";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn on_b(&mut self) {
|
||||
self.outer_list = true;
|
||||
}
|
||||
|
||||
pub fn on_l(&mut self) {
|
||||
if self.outer_list {
|
||||
self.tabs.previous();
|
||||
self.set_sub_menu_items();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn on_r(&mut self) {
|
||||
if self.outer_list {
|
||||
self.tabs.next();
|
||||
self.set_sub_menu_items();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn on_up(&mut self) {
|
||||
if self.outer_list {
|
||||
self.menu_items.get_mut(self.tabs.items.get(self.tabs.state.selected().unwrap()).unwrap()).unwrap().previous();
|
||||
self.set_sub_menu_items();
|
||||
} else {
|
||||
self.sub_menu_previous();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn on_down(&mut self) {
|
||||
if self.outer_list {
|
||||
self.menu_items.get_mut(self.tabs.items.get(self.tabs.state.selected().unwrap()).unwrap()).unwrap().next();
|
||||
self.set_sub_menu_items();
|
||||
} else {
|
||||
self.sub_menu_next();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn on_left(&mut self) {
|
||||
if self.outer_list {
|
||||
self.menu_items.get_mut(self.tabs.items.get(self.tabs.state.selected().unwrap()).unwrap()).unwrap().previous_list();
|
||||
self.set_sub_menu_items();
|
||||
} else {
|
||||
self.sub_menu_previous_list();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn on_right(&mut self) {
|
||||
if self.outer_list {
|
||||
self.menu_items.get_mut(self.tabs.items.get(self.tabs.state.selected().unwrap()).unwrap()).unwrap().next_list();
|
||||
self.set_sub_menu_items();
|
||||
} else {
|
||||
self.sub_menu_next_list();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ui<B: Backend>(f: &mut Frame<B>, app: &mut App) -> String {
|
||||
let app_tabs = &app.tabs;
|
||||
let tab_selected = app_tabs.state.selected().unwrap();
|
||||
let titles = app_tabs.items.iter().cloned().enumerate().map(|(idx, tab)|{
|
||||
if idx == tab_selected {
|
||||
Spans::from(">> ".to_owned() + tab)
|
||||
} else {
|
||||
Spans::from(" ".to_owned() + tab)
|
||||
}
|
||||
}).collect();
|
||||
let tabs = Tabs::new(titles)
|
||||
.block(Block::default().title("Ultimate Training Modpack Menu"))
|
||||
.style(Style::default().fg(Color::White))
|
||||
.highlight_style(Style::default().fg(Color::Yellow))
|
||||
.divider("|")
|
||||
.select(tab_selected);
|
||||
|
||||
let vertical_chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([
|
||||
Constraint::Length(2),
|
||||
Constraint::Max(10),
|
||||
Constraint::Length(2)].as_ref())
|
||||
.split(f.size());
|
||||
|
||||
let list_chunks = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints([Constraint::Percentage(33), Constraint::Percentage(32), Constraint::Percentage(33)].as_ref())
|
||||
.split(vertical_chunks[1]);
|
||||
|
||||
f.render_widget(tabs, vertical_chunks[0]);
|
||||
|
||||
if app.outer_list {
|
||||
let tab_selected = app.tab_selected();
|
||||
let mut item_help = None;
|
||||
for list_section in 0..app.menu_items.get(tab_selected).unwrap().lists.len() {
|
||||
let stateful_list = &app.menu_items.get(tab_selected).unwrap().lists[list_section];
|
||||
let items: Vec<ListItem> = stateful_list
|
||||
.items
|
||||
.iter()
|
||||
.map(|i| {
|
||||
let lines = vec![Spans::from(
|
||||
if stateful_list.state.selected().is_some() {
|
||||
i.title.to_owned()
|
||||
} else {
|
||||
" ".to_owned() + i.title
|
||||
})];
|
||||
ListItem::new(lines).style(Style::default().fg(Color::Black).bg(Color::White))
|
||||
})
|
||||
.collect();
|
||||
|
||||
let list = List::new(items)
|
||||
.block(Block::default().title(if list_section == 0 { "Options" } else { "" }))
|
||||
.highlight_style(
|
||||
Style::default()
|
||||
.bg(Color::LightBlue)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
)
|
||||
.highlight_symbol(">> ");
|
||||
|
||||
let mut state = stateful_list.state.clone();
|
||||
if state.selected().is_some() {
|
||||
item_help = Some(stateful_list.items[state.selected().unwrap()].help_text);
|
||||
}
|
||||
|
||||
f.render_stateful_widget(list, list_chunks[list_section], &mut state);
|
||||
}
|
||||
|
||||
let help_paragraph = Paragraph::new(
|
||||
item_help.unwrap_or("").replace("\"", "") +
|
||||
"\nA: Enter sub-menu | B: Exit menu | ZL/ZR: Next tab | R: Save defaults"
|
||||
);
|
||||
f.render_widget(help_paragraph, vertical_chunks[2]);
|
||||
} else {
|
||||
let (title, help_text, mut sub_menu_str_lists) = app.sub_menu_strs_and_states();
|
||||
for list_section in 0..sub_menu_str_lists.len() {
|
||||
let sub_menu_str = sub_menu_str_lists[list_section].0.clone();
|
||||
let sub_menu_state = &mut sub_menu_str_lists[list_section].1;
|
||||
let values_items: Vec<ListItem> = sub_menu_str.iter().map(|s| {
|
||||
ListItem::new(
|
||||
vec![
|
||||
Spans::from((if s.0 == "is-appear" { "X " } else { " " }).to_owned() + s.1)
|
||||
]
|
||||
)
|
||||
}).collect();
|
||||
|
||||
let values_list = List::new(values_items)
|
||||
.block(Block::default().title(if list_section == 0 { title } else { "" }))
|
||||
.start_corner(Corner::TopLeft)
|
||||
.highlight_style(
|
||||
Style::default()
|
||||
.bg(Color::LightGreen)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
)
|
||||
.highlight_symbol(">> ");
|
||||
f.render_stateful_widget(values_list, list_chunks[list_section], sub_menu_state);
|
||||
}
|
||||
|
||||
let help_paragraph = Paragraph::new(
|
||||
help_text.replace("\"", "") +
|
||||
"\nA: Select toggle | B: Exit submenu"
|
||||
);
|
||||
f.render_widget(help_paragraph, vertical_chunks[2]);
|
||||
}
|
||||
|
||||
|
||||
let mut url = "http://localhost/".to_owned();
|
||||
let mut settings = HashMap::new();
|
||||
|
||||
// Collect settings for toggles
|
||||
for key in app.menu_items.keys() {
|
||||
for list in &app.menu_items.get(key).unwrap().lists {
|
||||
for sub_menu in &list.items {
|
||||
let mut val = String::new();
|
||||
sub_menu.toggles.iter()
|
||||
.filter(|t| t.checked == "is-appear")
|
||||
.for_each(|t| val.push_str(format!("{},", t.value).as_str()));
|
||||
|
||||
sub_menu.onoffselector.iter()
|
||||
.for_each(|o| {
|
||||
val.push_str(
|
||||
format!("{}", if o.checked == "is-appear" { 1 } else { 0 }).as_str())
|
||||
});
|
||||
settings.insert(sub_menu.id, val);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
url.push_str("?");
|
||||
settings.iter()
|
||||
.for_each(|(section, val)| url.push_str(format!("{}={}&", section, val).as_str()));
|
||||
url
|
||||
|
||||
// TODO: Add saveDefaults
|
||||
// if (document.getElementById("saveDefaults").checked) {
|
||||
// url += "save_defaults=1";
|
||||
// } else {
|
||||
// url = url.slice(0, -1);
|
||||
// }
|
||||
}
|
||||
191
training_mod_tui/src/list.rs
Normal file
191
training_mod_tui/src/list.rs
Normal file
@@ -0,0 +1,191 @@
|
||||
use tui::widgets::ListState;
|
||||
|
||||
pub struct MultiStatefulList<T> {
|
||||
pub lists: Vec<StatefulList<T>>,
|
||||
pub state: usize,
|
||||
pub total_len: usize
|
||||
}
|
||||
|
||||
impl<T: Clone> MultiStatefulList<T> {
|
||||
pub fn selected_list_item(&mut self) -> &mut T {
|
||||
let (list_section, list_idx) = self.idx_to_list_idx(self.state);
|
||||
&mut self
|
||||
.lists[list_section]
|
||||
.items[list_idx]
|
||||
}
|
||||
|
||||
pub fn idx_to_list_idx(&self, idx: usize) -> (usize, usize) {
|
||||
for list_section in 0..self.lists.len() {
|
||||
let list_section_min_idx = (self.total_len as f32 / self.lists.len() as f32).ceil() as usize * list_section;
|
||||
let list_section_max_idx = std::cmp::min(
|
||||
(self.total_len as f32 / self.lists.len() as f32).ceil() as usize * (list_section + 1),
|
||||
self.total_len);
|
||||
if (list_section_min_idx..list_section_max_idx).contains(&idx) {
|
||||
// println!("\n{}: ({}, {})", idx, list_section_min_idx, list_section_max_idx);
|
||||
return (list_section, idx - list_section_min_idx)
|
||||
}
|
||||
}
|
||||
(0, 0)
|
||||
}
|
||||
|
||||
fn list_idx_to_idx(&self, list_idx: (usize, usize)) -> usize {
|
||||
let list_section = list_idx.0;
|
||||
let mut list_idx = list_idx.1;
|
||||
for list_section in 0..list_section {
|
||||
list_idx += self.lists[list_section].items.len();
|
||||
}
|
||||
list_idx
|
||||
}
|
||||
|
||||
pub fn with_items(items: Vec<T>, num_lists: usize) -> MultiStatefulList<T> {
|
||||
let lists = (0..num_lists).map(|list_section| {
|
||||
let list_section_min_idx = (items.len() as f32 / num_lists as f32).ceil() as usize * list_section;
|
||||
let list_section_max_idx = std::cmp::min(
|
||||
(items.len() as f32 / num_lists as f32).ceil() as usize * (list_section + 1),
|
||||
items.len());
|
||||
let mut state = ListState::default();
|
||||
if list_section == 0 {
|
||||
// Enforce state as first of list
|
||||
state.select(Some(0));
|
||||
}
|
||||
StatefulList {
|
||||
state: state,
|
||||
items: items[list_section_min_idx..list_section_max_idx].to_vec(),
|
||||
}
|
||||
}).collect();
|
||||
let total_len = items.len();
|
||||
MultiStatefulList {
|
||||
lists: lists,
|
||||
total_len: total_len,
|
||||
state: 0
|
||||
}
|
||||
}
|
||||
|
||||
pub fn next(&mut self) {
|
||||
let (list_section, _) = self.idx_to_list_idx(self.state);
|
||||
let (next_list_section, next_list_idx) = self.idx_to_list_idx(self.state+1);
|
||||
|
||||
if list_section != next_list_section {
|
||||
self.lists[list_section].unselect();
|
||||
}
|
||||
let state;
|
||||
if self.state + 1 >= self.total_len {
|
||||
state = (0, 0);
|
||||
} else {
|
||||
state = (next_list_section, next_list_idx);
|
||||
}
|
||||
|
||||
self.lists[state.0].state.select(Some(state.1));
|
||||
self.state = self.list_idx_to_idx(state);
|
||||
}
|
||||
|
||||
pub fn previous(&mut self) {
|
||||
let (list_section, _) = self.idx_to_list_idx(self.state);
|
||||
let (last_list_section, last_list_idx) = (self.lists.len() - 1, self.lists[self.lists.len() - 1].items.len() - 1);
|
||||
|
||||
self.lists[list_section].unselect();
|
||||
let state;
|
||||
if self.state == 0 {
|
||||
state = (last_list_section, last_list_idx);
|
||||
} else {
|
||||
let (prev_list_section, prev_list_idx) = self.idx_to_list_idx(self.state - 1);
|
||||
state = (prev_list_section, prev_list_idx);
|
||||
}
|
||||
|
||||
self.lists[state.0].state.select(Some(state.1));
|
||||
self.state = self.list_idx_to_idx(state);
|
||||
}
|
||||
|
||||
pub fn next_list(&mut self) {
|
||||
let (list_section, list_idx) = self.idx_to_list_idx(self.state);
|
||||
let next_list_section = (list_section + 1) % self.lists.len();
|
||||
let next_list_idx;
|
||||
if list_idx > self.lists[next_list_section].items.len() - 1 {
|
||||
next_list_idx = self.lists[next_list_section].items.len() - 1;
|
||||
} else {
|
||||
next_list_idx = list_idx;
|
||||
}
|
||||
|
||||
if list_section != next_list_section {
|
||||
self.lists[list_section].unselect();
|
||||
}
|
||||
let state = (next_list_section, next_list_idx);
|
||||
|
||||
self.lists[state.0].state.select(Some(state.1));
|
||||
self.state = self.list_idx_to_idx(state);
|
||||
}
|
||||
|
||||
pub fn previous_list(&mut self) {
|
||||
let (list_section, list_idx) = self.idx_to_list_idx(self.state);
|
||||
let prev_list_section;
|
||||
if list_section == 0 {
|
||||
prev_list_section = self.lists.len() - 1;
|
||||
} else {
|
||||
prev_list_section = list_section - 1;
|
||||
}
|
||||
|
||||
let prev_list_idx;
|
||||
if list_idx > self.lists[prev_list_section].items.len() - 1 {
|
||||
prev_list_idx = self.lists[prev_list_section].items.len() - 1;
|
||||
} else {
|
||||
prev_list_idx = list_idx;
|
||||
}
|
||||
|
||||
if list_section != prev_list_section {
|
||||
self.lists[list_section].unselect();
|
||||
}
|
||||
let state = (prev_list_section, prev_list_idx);
|
||||
|
||||
self.lists[state.0].state.select(Some(state.1));
|
||||
self.state = self.list_idx_to_idx(state);
|
||||
}
|
||||
}
|
||||
|
||||
pub struct StatefulList<T> {
|
||||
pub state: ListState,
|
||||
pub items: Vec<T>,
|
||||
}
|
||||
|
||||
impl<T> StatefulList<T> {
|
||||
pub fn with_items(items: Vec<T>) -> StatefulList<T> {
|
||||
let mut state = ListState::default();
|
||||
// Enforce state as first of list
|
||||
state.select(Some(0));
|
||||
StatefulList {
|
||||
state: state,
|
||||
items,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn next(&mut self) {
|
||||
let i = match self.state.selected() {
|
||||
Some(i) => {
|
||||
if i >= self.items.len() - 1 {
|
||||
0
|
||||
} else {
|
||||
i + 1
|
||||
}
|
||||
}
|
||||
None => 0,
|
||||
};
|
||||
self.state.select(Some(i));
|
||||
}
|
||||
|
||||
pub fn previous(&mut self) {
|
||||
let i = match self.state.selected() {
|
||||
Some(i) => {
|
||||
if i == 0 {
|
||||
self.items.len() - 1
|
||||
} else {
|
||||
i - 1
|
||||
}
|
||||
}
|
||||
None => 0,
|
||||
};
|
||||
self.state.select(Some(i));
|
||||
}
|
||||
|
||||
pub fn unselect(&mut self) {
|
||||
self.state.select(None);
|
||||
}
|
||||
}
|
||||
113
training_mod_tui/src/main.rs
Normal file
113
training_mod_tui/src/main.rs
Normal file
@@ -0,0 +1,113 @@
|
||||
#[cfg(feature = "has_terminal")]
|
||||
use crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
|
||||
#[cfg(feature = "has_terminal")]
|
||||
use tui::backend::CrosstermBackend;
|
||||
#[cfg(feature = "has_terminal")]
|
||||
use std::{
|
||||
io,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use std::error::Error;
|
||||
use tui::Terminal;
|
||||
|
||||
use training_mod_consts::*;
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
let menu;
|
||||
unsafe {
|
||||
menu = get_menu();
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "has_terminal"))] {
|
||||
let mut app = training_mod_tui::App::new(menu);
|
||||
let backend = tui::backend::TestBackend::new(75, 15);
|
||||
let mut terminal = Terminal::new(backend)?;
|
||||
let mut state = tui::widgets::ListState::default();
|
||||
state.select(Some(1));
|
||||
let mut url = String::new();
|
||||
let frame_res = terminal.draw(|f| url = training_mod_tui::ui(f, &mut app))?;
|
||||
|
||||
for (i, cell) in frame_res.buffer.content().into_iter().enumerate() {
|
||||
print!("{}", cell.symbol);
|
||||
if i % frame_res.area.width as usize == frame_res.area.width as usize - 1 {
|
||||
print!("\n");
|
||||
}
|
||||
}
|
||||
println!();
|
||||
|
||||
println!("URL: {}", url);
|
||||
}
|
||||
|
||||
#[cfg(feature = "has_terminal")] {
|
||||
let app = training_mod_tui::App::new(menu);
|
||||
|
||||
// setup terminal
|
||||
enable_raw_mode()?;
|
||||
let mut stdout = io::stdout();
|
||||
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
|
||||
let backend = CrosstermBackend::new(stdout);
|
||||
let mut terminal = Terminal::new(backend)?;
|
||||
|
||||
let tick_rate = Duration::from_millis(250);
|
||||
let res = run_app(&mut terminal, app, tick_rate);
|
||||
|
||||
// restore terminal
|
||||
disable_raw_mode()?;
|
||||
execute!(
|
||||
terminal.backend_mut(),
|
||||
LeaveAlternateScreen,
|
||||
DisableMouseCapture
|
||||
)?;
|
||||
terminal.show_cursor()?;
|
||||
|
||||
if let Err(err) = res {
|
||||
println!("{:?}", err)
|
||||
} else {
|
||||
println!("URL: {}", res.as_ref().unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(feature = "has_terminal")]
|
||||
fn run_app<B: tui::backend::Backend>(
|
||||
terminal: &mut Terminal<B>,
|
||||
mut app: training_mod_tui::App,
|
||||
tick_rate: Duration,
|
||||
) -> io::Result<String> {
|
||||
let mut last_tick = Instant::now();
|
||||
let mut url = String::new();
|
||||
loop {
|
||||
terminal.draw(|f| url = training_mod_tui::ui(f, &mut app).clone())?;
|
||||
|
||||
let timeout = tick_rate
|
||||
.checked_sub(last_tick.elapsed())
|
||||
.unwrap_or_else(|| Duration::from_secs(0));
|
||||
|
||||
if crossterm::event::poll(timeout)? {
|
||||
if let Event::Key(key) = event::read()? {
|
||||
match key.code {
|
||||
KeyCode::Char('q') => return Ok(url),
|
||||
KeyCode::Char('r') => app.on_r(),
|
||||
KeyCode::Char('l') => app.on_l(),
|
||||
KeyCode::Left => app.on_left(),
|
||||
KeyCode::Right => app.on_right(),
|
||||
KeyCode::Down => app.on_down(),
|
||||
KeyCode::Up => app.on_up(),
|
||||
KeyCode::Enter => app.on_a(),
|
||||
KeyCode::Backspace => app.on_b(),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
if last_tick.elapsed() >= tick_rate {
|
||||
last_tick = Instant::now();
|
||||
}
|
||||
}
|
||||
}
|
||||
12
training_mod_tui/training_mod_tui.iml
Normal file
12
training_mod_tui/training_mod_tui.iml
Normal file
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="RUST_MODULE" version="4">
|
||||
<component name="NewModuleRootManager" inherit-compiler-output="true">
|
||||
<exclude-output />
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/target" />
|
||||
</content>
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
||||
Reference in New Issue
Block a user