mirror of
https://github.com/imgurbot12/rmenu.git
synced 2024-11-13 04:43:46 +01:00
feat: much improved internal-state mgmt system
This commit is contained in:
parent
82897da0e2
commit
20ced56137
@ -113,8 +113,12 @@ impl Default for WindowConfig {
|
|||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
title: "RMenu - App Launcher".to_owned(),
|
title: "RMenu - App Launcher".to_owned(),
|
||||||
|
// size: LogicalSize {
|
||||||
|
// width: 700.0,
|
||||||
|
// height: 400.0,
|
||||||
|
// },
|
||||||
size: LogicalSize {
|
size: LogicalSize {
|
||||||
width: 700.0,
|
width: 1000.0,
|
||||||
height: 400.0,
|
height: 400.0,
|
||||||
},
|
},
|
||||||
position: LogicalPosition { x: 100.0, y: 100.0 },
|
position: LogicalPosition { x: 100.0, y: 100.0 },
|
||||||
@ -130,6 +134,8 @@ impl Default for WindowConfig {
|
|||||||
#[derive(Debug, PartialEq, Deserialize)]
|
#[derive(Debug, PartialEq, Deserialize)]
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
|
pub page_size: usize,
|
||||||
|
pub page_load: f64,
|
||||||
pub use_icons: bool,
|
pub use_icons: bool,
|
||||||
pub search_regex: bool,
|
pub search_regex: bool,
|
||||||
pub ignore_case: bool,
|
pub ignore_case: bool,
|
||||||
@ -141,6 +147,8 @@ pub struct Config {
|
|||||||
impl Default for Config {
|
impl Default for Config {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
page_size: 50,
|
||||||
|
page_load: 0.8,
|
||||||
use_icons: true,
|
use_icons: true,
|
||||||
search_regex: false,
|
search_regex: false,
|
||||||
ignore_case: true,
|
ignore_case: true,
|
||||||
|
146
rmenu/src/gui.rs
146
rmenu/src/gui.rs
@ -4,10 +4,8 @@ use dioxus::prelude::*;
|
|||||||
use keyboard_types::{Code, Modifiers};
|
use keyboard_types::{Code, Modifiers};
|
||||||
use rmenu_plugin::Entry;
|
use rmenu_plugin::Entry;
|
||||||
|
|
||||||
use crate::config::{Config, Keybind};
|
use crate::config::Keybind;
|
||||||
use crate::exec::execute;
|
use crate::state::{AppState, KeyEvent};
|
||||||
use crate::search::new_searchfn;
|
|
||||||
use crate::state::PosTracker;
|
|
||||||
use crate::App;
|
use crate::App;
|
||||||
|
|
||||||
/// spawn and run the app on the configured platform
|
/// spawn and run the app on the configured platform
|
||||||
@ -35,11 +33,11 @@ pub fn run(app: App) {
|
|||||||
|
|
||||||
#[derive(PartialEq, Props)]
|
#[derive(PartialEq, Props)]
|
||||||
struct GEntry<'a> {
|
struct GEntry<'a> {
|
||||||
index: usize,
|
|
||||||
entry: &'a Entry,
|
|
||||||
config: &'a Config,
|
|
||||||
pos: usize,
|
pos: usize,
|
||||||
subpos: usize,
|
subpos: usize,
|
||||||
|
index: usize,
|
||||||
|
entry: &'a Entry,
|
||||||
|
state: AppState<'a>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// render a single result entry w/ the given information
|
/// render a single result entry w/ the given information
|
||||||
@ -75,6 +73,8 @@ fn TableEntry<'a>(cx: Scope<'a, GEntry<'a>>) -> Element<'a> {
|
|||||||
cx.render(rsx! {
|
cx.render(rsx! {
|
||||||
div {
|
div {
|
||||||
class: "action {act_class}",
|
class: "action {act_class}",
|
||||||
|
onclick: move |_| cx.props.state.set_position(cx.props.index, idx + 1),
|
||||||
|
ondblclick: |_| cx.props.state.set_event(KeyEvent::Exec),
|
||||||
div {
|
div {
|
||||||
class: "action-name",
|
class: "action-name",
|
||||||
"{action.name}"
|
"{action.name}"
|
||||||
@ -92,14 +92,10 @@ fn TableEntry<'a>(cx: Scope<'a, GEntry<'a>>) -> Element<'a> {
|
|||||||
div {
|
div {
|
||||||
id: "result-{cx.props.index}",
|
id: "result-{cx.props.index}",
|
||||||
class: "result {result_classes} {multi_classes}",
|
class: "result {result_classes} {multi_classes}",
|
||||||
ondblclick: |_| {
|
// onmouseenter: |_| cx.props.state.set_position(cx.props.index, 0),
|
||||||
let action = match cx.props.entry.actions.get(0) {
|
onclick: |_| cx.props.state.set_position(cx.props.index, 0),
|
||||||
Some(action) => action,
|
ondblclick: |_| cx.props.state.set_event(KeyEvent::Exec),
|
||||||
None => panic!("No Action Configured"),
|
if cx.props.state.config().use_icons {
|
||||||
};
|
|
||||||
execute(action);
|
|
||||||
},
|
|
||||||
if cx.props.config.use_icons {
|
|
||||||
cx.render(rsx! {
|
cx.render(rsx! {
|
||||||
div {
|
div {
|
||||||
class: "icon",
|
class: "icon",
|
||||||
@ -142,84 +138,76 @@ fn matches(bind: &Vec<Keybind>, mods: &Modifiers, key: &Code) -> bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// main application function/loop
|
/// main application function/loop
|
||||||
fn App(cx: Scope<App>) -> Element {
|
fn App<'a>(cx: Scope<App>) -> Element {
|
||||||
let quit = use_state(cx, || false);
|
let mut state = AppState::new(cx, cx.props);
|
||||||
let search = use_state(cx, || "".to_string());
|
|
||||||
|
|
||||||
// handle exit check
|
// log current position
|
||||||
if *quit.get() {
|
let search = state.search();
|
||||||
std::process::exit(0);
|
let (pos, subpos) = state.position();
|
||||||
}
|
|
||||||
|
|
||||||
// retrieve results and filter based on search
|
|
||||||
let searchfn = new_searchfn(&cx.props.config, &search);
|
|
||||||
let results: Vec<&Entry> = cx
|
|
||||||
.props
|
|
||||||
.entries
|
|
||||||
.iter()
|
|
||||||
.filter(|entry| searchfn(entry))
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
// retrieve results build and build position-tracker
|
|
||||||
let tracker = PosTracker::new(cx, results.clone());
|
|
||||||
let (pos, subpos) = tracker.position();
|
|
||||||
log::debug!("search: {search:?}, pos: {pos}, {subpos}");
|
log::debug!("search: {search:?}, pos: {pos}, {subpos}");
|
||||||
|
|
||||||
// keyboard events
|
// generate state tracker instances
|
||||||
|
let results = state.results(&cx.props.entries);
|
||||||
|
let s_updater = state.partial_copy();
|
||||||
|
let k_updater = state.partial_copy();
|
||||||
|
|
||||||
|
//TODO: consider implementing some sort of
|
||||||
|
// action channel reference to pass to keboard events
|
||||||
|
|
||||||
|
// build keyboard actions event handler
|
||||||
let keybinds = &cx.props.config.keybinds;
|
let keybinds = &cx.props.config.keybinds;
|
||||||
let keyboard_evt = move |evt: KeyboardEvent| {
|
let keyboard_controls = move |e: KeyboardEvent| {
|
||||||
let key = &evt.code();
|
let code = e.code();
|
||||||
let mods = &evt.modifiers();
|
let mods = e.modifiers();
|
||||||
log::debug!("key: {key:?} mods: {mods:?}");
|
if matches(&keybinds.exec, &mods, &code) {
|
||||||
if matches(&keybinds.exec, mods, key) {
|
k_updater.set_event(KeyEvent::Exec);
|
||||||
match tracker.action() {
|
} else if matches(&keybinds.exit, &mods, &code) {
|
||||||
Some(action) => execute(action),
|
k_updater.set_event(KeyEvent::Exit);
|
||||||
None => panic!("No Action Configured"),
|
} else if matches(&keybinds.move_up, &mods, &code) {
|
||||||
}
|
k_updater.set_event(KeyEvent::ShiftUp);
|
||||||
} else if matches(&keybinds.exit, mods, key) {
|
} else if matches(&keybinds.move_down, &mods, &code) {
|
||||||
quit.set(true);
|
k_updater.set_event(KeyEvent::ShiftDown);
|
||||||
} else if matches(&keybinds.move_up, mods, key) {
|
} else if matches(&keybinds.open_menu, &mods, &code) {
|
||||||
tracker.shift_up();
|
k_updater.set_event(KeyEvent::OpenMenu);
|
||||||
} else if matches(&keybinds.move_down, mods, key) {
|
} else if matches(&keybinds.close_menu, &mods, &code) {
|
||||||
tracker.shift_down();
|
k_updater.set_event(KeyEvent::CloseMenu);
|
||||||
} else if matches(&keybinds.open_menu, mods, key) {
|
|
||||||
tracker.open_menu();
|
|
||||||
} else if matches(&keybinds.close_menu, mods, key) {
|
|
||||||
tracker.close_menu();
|
|
||||||
}
|
}
|
||||||
// always set focus back on input
|
|
||||||
focus(cx);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// pre-render results into elements
|
// handle keyboard events
|
||||||
let results_rendered: Vec<Element> = results
|
state.handle_events(cx);
|
||||||
.iter()
|
|
||||||
.enumerate()
|
// render results objects
|
||||||
.map(|(index, entry)| {
|
let rendered_results = results.iter().enumerate().map(|(i, e)| {
|
||||||
cx.render(rsx! {
|
let state = state.partial_copy();
|
||||||
TableEntry{
|
cx.render(rsx! {
|
||||||
index: index,
|
TableEntry{
|
||||||
entry: entry,
|
pos: pos,
|
||||||
config: &cx.props.config,
|
subpos: subpos,
|
||||||
pos: pos,
|
index: i,
|
||||||
subpos: subpos,
|
entry: e,
|
||||||
}
|
state: state,
|
||||||
})
|
}
|
||||||
})
|
})
|
||||||
.collect();
|
});
|
||||||
|
|
||||||
cx.render(rsx! {
|
cx.render(rsx! {
|
||||||
style { "{cx.props.css}" }
|
style { "{cx.props.css}" }
|
||||||
div {
|
div {
|
||||||
onkeydown: keyboard_evt,
|
|
||||||
onclick: |_| focus(cx),
|
onclick: |_| focus(cx),
|
||||||
input {
|
onkeydown: keyboard_controls,
|
||||||
id: "search",
|
div {
|
||||||
value: "{search}",
|
class: "navbar",
|
||||||
oninput: move |evt| search.set(evt.value.clone()),
|
input {
|
||||||
|
id: "search",
|
||||||
|
value: "{search}",
|
||||||
|
oninput: move |evt| s_updater.set_search(evt.value.clone()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
div {
|
||||||
|
class: "results",
|
||||||
|
rendered_results.into_iter()
|
||||||
}
|
}
|
||||||
results_rendered.into_iter()
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -218,13 +218,14 @@ fn main() -> Result<(), RMenuError> {
|
|||||||
if std::env::var("RUST_LOG").is_ok() {
|
if std::env::var("RUST_LOG").is_ok() {
|
||||||
env_logger::init();
|
env_logger::init();
|
||||||
}
|
}
|
||||||
|
// parse cli / config / application-settings
|
||||||
|
let app = Args::parse_app()?;
|
||||||
// change directory to configuration dir
|
// change directory to configuration dir
|
||||||
let cfgdir = shellexpand::tilde(CONFIG_DIR).to_string();
|
let cfgdir = shellexpand::tilde(CONFIG_DIR).to_string();
|
||||||
if let Err(err) = std::env::set_current_dir(&cfgdir) {
|
if let Err(err) = std::env::set_current_dir(&cfgdir) {
|
||||||
log::error!("failed to change directory: {err:?}");
|
log::error!("failed to change directory: {err:?}");
|
||||||
}
|
}
|
||||||
// parse cli / config / application-settings
|
// run gui
|
||||||
let app = Args::parse_app()?;
|
|
||||||
gui::run(app);
|
gui::run(app);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -1,76 +1,237 @@
|
|||||||
//! GUI Application State Trackers and Utilities
|
use dioxus::prelude::{use_ref, Scope, UseRef};
|
||||||
use dioxus::prelude::{use_state, Scope, UseState};
|
use rmenu_plugin::Entry;
|
||||||
use rmenu_plugin::{Action, Entry};
|
|
||||||
|
|
||||||
|
use crate::config::Config;
|
||||||
|
use crate::exec::execute;
|
||||||
|
use crate::search::new_searchfn;
|
||||||
use crate::App;
|
use crate::App;
|
||||||
|
|
||||||
#[derive(PartialEq)]
|
#[inline]
|
||||||
pub struct PosTracker<'a> {
|
fn scroll<T>(cx: Scope<T>, pos: usize) {
|
||||||
pos: &'a UseState<usize>,
|
let eval = dioxus_desktop::use_eval(cx);
|
||||||
subpos: &'a UseState<usize>,
|
let js = format!("document.getElementById(`result-{pos}`).scrollIntoView(false)");
|
||||||
results: Vec<&'a Entry>,
|
eval(js);
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> PosTracker<'a> {
|
#[derive(Debug, PartialEq, Clone)]
|
||||||
pub fn new(cx: Scope<'a, App>, results: Vec<&'a Entry>) -> Self {
|
pub enum KeyEvent {
|
||||||
let pos = use_state(cx, || 0);
|
Exec,
|
||||||
let subpos = use_state(cx, || 0);
|
Exit,
|
||||||
Self {
|
ShiftUp,
|
||||||
pos,
|
ShiftDown,
|
||||||
subpos,
|
OpenMenu,
|
||||||
results,
|
CloseMenu,
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
pub struct InnerState {
|
||||||
|
pos: usize,
|
||||||
|
subpos: usize,
|
||||||
|
page: usize,
|
||||||
|
search: String,
|
||||||
|
event: Option<KeyEvent>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InnerState {
|
||||||
/// Move X Primary Results Upwards
|
/// Move X Primary Results Upwards
|
||||||
pub fn move_up(&self, x: usize) {
|
pub fn move_up(&mut self, x: usize) {
|
||||||
self.subpos.set(0);
|
self.subpos = 0;
|
||||||
self.pos.modify(|v| if v >= &x { v - x } else { 0 })
|
self.pos = std::cmp::max(self.pos, x) - x;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Move X Primary Results Downwards
|
/// Move X Primary Results Downwards
|
||||||
pub fn move_down(&self, x: usize) {
|
pub fn move_down(&mut self, x: usize, max: usize) {
|
||||||
let max = std::cmp::max(self.results.len(), 1);
|
self.subpos = 0;
|
||||||
self.subpos.set(0);
|
self.pos = std::cmp::min(self.pos + x, max - 1)
|
||||||
self.pos.modify(|v| std::cmp::min(v + x, max - 1))
|
|
||||||
}
|
|
||||||
/// Get Current Position/SubPosition
|
|
||||||
pub fn position(&self) -> (usize, usize) {
|
|
||||||
(self.pos.get().clone(), self.subpos.get().clone())
|
|
||||||
}
|
|
||||||
/// Get Action Linked To The Current Position
|
|
||||||
pub fn action(&self) -> Option<&Action> {
|
|
||||||
let (pos, subpos) = self.position();
|
|
||||||
self.results[pos].actions.get(subpos)
|
|
||||||
}
|
|
||||||
/// Move Position To SubMenu if it Exists
|
|
||||||
pub fn open_menu(&self) {
|
|
||||||
let index = *self.pos.get();
|
|
||||||
let result = &self.results[index];
|
|
||||||
if result.actions.len() > 0 {
|
|
||||||
self.subpos.set(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Reset and Close SubMenu Position
|
|
||||||
pub fn close_menu(&self) {
|
|
||||||
self.subpos.set(0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Move Up Once With Context of SubMenu
|
/// Move Up Once With Context of SubMenu
|
||||||
pub fn shift_up(&self) {
|
pub fn shift_up(&mut self) {
|
||||||
if self.subpos.get() > &0 {
|
if self.subpos > 0 {
|
||||||
self.subpos.modify(|v| v - 1);
|
self.subpos -= 1;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
self.move_up(1)
|
self.move_up(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Move Down Once With Context of SubMenu
|
/// Move Down Once With Context of SubMenu
|
||||||
pub fn shift_down(&self) {
|
pub fn shift_down(&mut self, results: &Vec<&Entry>) {
|
||||||
let index = *self.pos.get();
|
if let Some(result) = results.get(self.pos) {
|
||||||
if let Some(result) = &self.results.get(index) {
|
if self.subpos > 0 && self.subpos < result.actions.len() - 1 {
|
||||||
let subpos = *self.subpos.get();
|
self.subpos += 1;
|
||||||
if subpos > 0 && subpos < result.actions.len() - 1 {
|
|
||||||
self.subpos.modify(|v| v + 1);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.move_down(1)
|
let max = std::cmp::max(results.len(), 1);
|
||||||
|
self.move_down(1, max);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(PartialEq)]
|
||||||
|
pub struct AppState<'a> {
|
||||||
|
state: &'a UseRef<InnerState>,
|
||||||
|
app: &'a App,
|
||||||
|
results: Vec<&'a Entry>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> AppState<'a> {
|
||||||
|
/// Spawn new Application State Tracker
|
||||||
|
pub fn new<T>(cx: Scope<'a, T>, app: &'a App) -> Self {
|
||||||
|
Self {
|
||||||
|
state: use_ref(cx, || InnerState {
|
||||||
|
pos: 0,
|
||||||
|
subpos: 0,
|
||||||
|
page: 0,
|
||||||
|
search: "".to_string(),
|
||||||
|
event: None,
|
||||||
|
}),
|
||||||
|
app,
|
||||||
|
results: vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create Partial Copy of Self (Not Including Results)
|
||||||
|
pub fn partial_copy(&self) -> Self {
|
||||||
|
Self {
|
||||||
|
state: self.state,
|
||||||
|
app: self.app,
|
||||||
|
results: vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Retrieve Configuration
|
||||||
|
#[inline]
|
||||||
|
pub fn config(&self) -> &Config {
|
||||||
|
&self.app.config
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Retrieve Current Position State
|
||||||
|
#[inline]
|
||||||
|
pub fn position(&self) -> (usize, usize) {
|
||||||
|
self.state.with(|s| (s.pos, s.subpos))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Retrieve Current Search String
|
||||||
|
#[inline]
|
||||||
|
pub fn search(&self) -> String {
|
||||||
|
self.state.with(|s| s.search.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Execute the Current Action
|
||||||
|
pub fn execute(&self) {
|
||||||
|
let (pos, subpos) = self.position();
|
||||||
|
println!("double click {pos} {subpos}");
|
||||||
|
let Some(result) = self.results.get(pos) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
println!("result: {result:?}");
|
||||||
|
let Some(action) = result.actions.get(subpos) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
println!("action: {action:?}");
|
||||||
|
execute(action);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set Current Key/Action for Later Evaluation
|
||||||
|
#[inline]
|
||||||
|
pub fn set_event(&self, event: KeyEvent) {
|
||||||
|
self.state.with_mut(|s| s.event = Some(event));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// React to Previously Activated KeyEvents
|
||||||
|
pub fn handle_events(&self, cx: Scope<'a, App>) {
|
||||||
|
match self.state.with(|s| s.event.clone()) {
|
||||||
|
None => {}
|
||||||
|
Some(event) => {
|
||||||
|
match event {
|
||||||
|
KeyEvent::Exit => std::process::exit(0),
|
||||||
|
KeyEvent::Exec => self.execute(),
|
||||||
|
KeyEvent::OpenMenu => self.open_menu(),
|
||||||
|
KeyEvent::CloseMenu => self.close_menu(),
|
||||||
|
KeyEvent::ShiftUp => {
|
||||||
|
self.shift_up();
|
||||||
|
scroll(cx, self.position().0)
|
||||||
|
}
|
||||||
|
KeyEvent::ShiftDown => {
|
||||||
|
self.shift_down();
|
||||||
|
scroll(cx, self.position().0)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
self.state.with_mut(|s| s.event = None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generate and return Results PTR
|
||||||
|
pub fn results(&mut self, entries: &'a Vec<Entry>) -> Vec<&'a Entry> {
|
||||||
|
let ratio = self.app.config.page_load;
|
||||||
|
let page_size = self.app.config.page_size;
|
||||||
|
let (pos, page, search) = self.state.with(|s| (s.pos, s.page, s.search.clone()));
|
||||||
|
// determine current page based on position and configuration
|
||||||
|
let next = (pos % page_size) as f64 / page_size as f64 > ratio;
|
||||||
|
let pos_page = (pos + 1) / page_size + 1 + next as usize;
|
||||||
|
let new_page = std::cmp::max(pos_page, page);
|
||||||
|
let index = page_size * new_page;
|
||||||
|
// update page counter if higher than before
|
||||||
|
if new_page > page {
|
||||||
|
self.state.with_mut(|s| s.page = new_page);
|
||||||
|
}
|
||||||
|
// render results and stop at page-limit
|
||||||
|
let sfn = new_searchfn(&self.app.config, &search);
|
||||||
|
self.results = entries.iter().filter(|e| sfn(e)).take(index).collect();
|
||||||
|
self.results.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update Search and Reset Position
|
||||||
|
pub fn set_search(&self, search: String) {
|
||||||
|
self.state.with_mut(|s| {
|
||||||
|
s.pos = 0;
|
||||||
|
s.subpos = 0;
|
||||||
|
s.search = search;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Manually Set Position/SubPosition (with Click)
|
||||||
|
pub fn set_position(&self, pos: usize, subpos: usize) {
|
||||||
|
self.state.with_mut(|s| {
|
||||||
|
s.pos = pos;
|
||||||
|
s.subpos = subpos;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Automatically Increase PageCount When Nearing Bottom
|
||||||
|
pub fn scroll_down(&self) {
|
||||||
|
self.state.with_mut(|s| {
|
||||||
|
if self.app.config.page_size * s.page < self.app.entries.len() {
|
||||||
|
s.page += 1;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Move Position To SubMenu if it Exists
|
||||||
|
pub fn open_menu(&self) {
|
||||||
|
let pos = self.state.with(|s| s.pos);
|
||||||
|
if let Some(result) = self.results.get(pos) {
|
||||||
|
if result.actions.len() > 1 {
|
||||||
|
self.state.with_mut(|s| s.subpos += 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset and Close SubMenu Position
|
||||||
|
#[inline]
|
||||||
|
pub fn close_menu(&self) {
|
||||||
|
self.state.with_mut(|s| s.subpos = 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Move Up Once With Context of SubMenu
|
||||||
|
#[inline]
|
||||||
|
pub fn shift_up(&self) {
|
||||||
|
self.state.with_mut(|s| s.shift_up());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Move Down Once With Context of SubMenu
|
||||||
|
#[inline]
|
||||||
|
pub fn shift_down(&self) {
|
||||||
|
self.state.with_mut(|s| s.shift_down(&self.results))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user