feat: much improved internal-state mgmt system

This commit is contained in:
imgurbot12 2023-07-24 21:24:18 -07:00
parent 82897da0e2
commit 20ced56137
4 changed files with 297 additions and 139 deletions

View File

@ -113,8 +113,12 @@ impl Default for WindowConfig {
fn default() -> Self {
Self {
title: "RMenu - App Launcher".to_owned(),
// size: LogicalSize {
// width: 700.0,
// height: 400.0,
// },
size: LogicalSize {
width: 700.0,
width: 1000.0,
height: 400.0,
},
position: LogicalPosition { x: 100.0, y: 100.0 },
@ -130,6 +134,8 @@ impl Default for WindowConfig {
#[derive(Debug, PartialEq, Deserialize)]
#[serde(default)]
pub struct Config {
pub page_size: usize,
pub page_load: f64,
pub use_icons: bool,
pub search_regex: bool,
pub ignore_case: bool,
@ -141,6 +147,8 @@ pub struct Config {
impl Default for Config {
fn default() -> Self {
Self {
page_size: 50,
page_load: 0.8,
use_icons: true,
search_regex: false,
ignore_case: true,

View File

@ -4,10 +4,8 @@ use dioxus::prelude::*;
use keyboard_types::{Code, Modifiers};
use rmenu_plugin::Entry;
use crate::config::{Config, Keybind};
use crate::exec::execute;
use crate::search::new_searchfn;
use crate::state::PosTracker;
use crate::config::Keybind;
use crate::state::{AppState, KeyEvent};
use crate::App;
/// spawn and run the app on the configured platform
@ -35,11 +33,11 @@ pub fn run(app: App) {
#[derive(PartialEq, Props)]
struct GEntry<'a> {
index: usize,
entry: &'a Entry,
config: &'a Config,
pos: usize,
subpos: usize,
index: usize,
entry: &'a Entry,
state: AppState<'a>,
}
/// 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! {
div {
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 {
class: "action-name",
"{action.name}"
@ -92,14 +92,10 @@ fn TableEntry<'a>(cx: Scope<'a, GEntry<'a>>) -> Element<'a> {
div {
id: "result-{cx.props.index}",
class: "result {result_classes} {multi_classes}",
ondblclick: |_| {
let action = match cx.props.entry.actions.get(0) {
Some(action) => action,
None => panic!("No Action Configured"),
};
execute(action);
},
if cx.props.config.use_icons {
// onmouseenter: |_| cx.props.state.set_position(cx.props.index, 0),
onclick: |_| cx.props.state.set_position(cx.props.index, 0),
ondblclick: |_| cx.props.state.set_event(KeyEvent::Exec),
if cx.props.state.config().use_icons {
cx.render(rsx! {
div {
class: "icon",
@ -142,84 +138,76 @@ fn matches(bind: &Vec<Keybind>, mods: &Modifiers, key: &Code) -> bool {
}
/// main application function/loop
fn App(cx: Scope<App>) -> Element {
let quit = use_state(cx, || false);
let search = use_state(cx, || "".to_string());
fn App<'a>(cx: Scope<App>) -> Element {
let mut state = AppState::new(cx, cx.props);
// handle exit check
if *quit.get() {
std::process::exit(0);
}
// 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 current position
let search = state.search();
let (pos, subpos) = state.position();
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 keyboard_evt = move |evt: KeyboardEvent| {
let key = &evt.code();
let mods = &evt.modifiers();
log::debug!("key: {key:?} mods: {mods:?}");
if matches(&keybinds.exec, mods, key) {
match tracker.action() {
Some(action) => execute(action),
None => panic!("No Action Configured"),
}
} else if matches(&keybinds.exit, mods, key) {
quit.set(true);
} else if matches(&keybinds.move_up, mods, key) {
tracker.shift_up();
} else if matches(&keybinds.move_down, mods, key) {
tracker.shift_down();
} else if matches(&keybinds.open_menu, mods, key) {
tracker.open_menu();
} else if matches(&keybinds.close_menu, mods, key) {
tracker.close_menu();
let keyboard_controls = move |e: KeyboardEvent| {
let code = e.code();
let mods = e.modifiers();
if matches(&keybinds.exec, &mods, &code) {
k_updater.set_event(KeyEvent::Exec);
} else if matches(&keybinds.exit, &mods, &code) {
k_updater.set_event(KeyEvent::Exit);
} else if matches(&keybinds.move_up, &mods, &code) {
k_updater.set_event(KeyEvent::ShiftUp);
} else if matches(&keybinds.move_down, &mods, &code) {
k_updater.set_event(KeyEvent::ShiftDown);
} else if matches(&keybinds.open_menu, &mods, &code) {
k_updater.set_event(KeyEvent::OpenMenu);
} else if matches(&keybinds.close_menu, &mods, &code) {
k_updater.set_event(KeyEvent::CloseMenu);
}
// always set focus back on input
focus(cx);
};
// pre-render results into elements
let results_rendered: Vec<Element> = results
.iter()
.enumerate()
.map(|(index, entry)| {
cx.render(rsx! {
TableEntry{
index: index,
entry: entry,
config: &cx.props.config,
pos: pos,
subpos: subpos,
}
})
// handle keyboard events
state.handle_events(cx);
// render results objects
let rendered_results = results.iter().enumerate().map(|(i, e)| {
let state = state.partial_copy();
cx.render(rsx! {
TableEntry{
pos: pos,
subpos: subpos,
index: i,
entry: e,
state: state,
}
})
.collect();
});
cx.render(rsx! {
style { "{cx.props.css}" }
div {
onkeydown: keyboard_evt,
onclick: |_| focus(cx),
input {
id: "search",
value: "{search}",
oninput: move |evt| search.set(evt.value.clone()),
onkeydown: keyboard_controls,
div {
class: "navbar",
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()
}
})
}

View File

@ -218,13 +218,14 @@ fn main() -> Result<(), RMenuError> {
if std::env::var("RUST_LOG").is_ok() {
env_logger::init();
}
// parse cli / config / application-settings
let app = Args::parse_app()?;
// change directory to configuration dir
let cfgdir = shellexpand::tilde(CONFIG_DIR).to_string();
if let Err(err) = std::env::set_current_dir(&cfgdir) {
log::error!("failed to change directory: {err:?}");
}
// parse cli / config / application-settings
let app = Args::parse_app()?;
// run gui
gui::run(app);
Ok(())
}

View File

@ -1,76 +1,237 @@
//! GUI Application State Trackers and Utilities
use dioxus::prelude::{use_state, Scope, UseState};
use rmenu_plugin::{Action, Entry};
use dioxus::prelude::{use_ref, Scope, UseRef};
use rmenu_plugin::Entry;
use crate::config::Config;
use crate::exec::execute;
use crate::search::new_searchfn;
use crate::App;
#[derive(PartialEq)]
pub struct PosTracker<'a> {
pos: &'a UseState<usize>,
subpos: &'a UseState<usize>,
results: Vec<&'a Entry>,
#[inline]
fn scroll<T>(cx: Scope<T>, pos: usize) {
let eval = dioxus_desktop::use_eval(cx);
let js = format!("document.getElementById(`result-{pos}`).scrollIntoView(false)");
eval(js);
}
impl<'a> PosTracker<'a> {
pub fn new(cx: Scope<'a, App>, results: Vec<&'a Entry>) -> Self {
let pos = use_state(cx, || 0);
let subpos = use_state(cx, || 0);
Self {
pos,
subpos,
results,
}
}
#[derive(Debug, PartialEq, Clone)]
pub enum KeyEvent {
Exec,
Exit,
ShiftUp,
ShiftDown,
OpenMenu,
CloseMenu,
}
pub struct InnerState {
pos: usize,
subpos: usize,
page: usize,
search: String,
event: Option<KeyEvent>,
}
impl InnerState {
/// Move X Primary Results Upwards
pub fn move_up(&self, x: usize) {
self.subpos.set(0);
self.pos.modify(|v| if v >= &x { v - x } else { 0 })
pub fn move_up(&mut self, x: usize) {
self.subpos = 0;
self.pos = std::cmp::max(self.pos, x) - x;
}
/// Move X Primary Results Downwards
pub fn move_down(&self, x: usize) {
let max = std::cmp::max(self.results.len(), 1);
self.subpos.set(0);
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);
pub fn move_down(&mut self, x: usize, max: usize) {
self.subpos = 0;
self.pos = std::cmp::min(self.pos + x, max - 1)
}
/// Move Up Once With Context of SubMenu
pub fn shift_up(&self) {
if self.subpos.get() > &0 {
self.subpos.modify(|v| v - 1);
pub fn shift_up(&mut self) {
if self.subpos > 0 {
self.subpos -= 1;
return;
}
self.move_up(1)
self.move_up(1);
}
/// Move Down Once With Context of SubMenu
pub fn shift_down(&self) {
let index = *self.pos.get();
if let Some(result) = &self.results.get(index) {
let subpos = *self.subpos.get();
if subpos > 0 && subpos < result.actions.len() - 1 {
self.subpos.modify(|v| v + 1);
pub fn shift_down(&mut self, results: &Vec<&Entry>) {
if let Some(result) = results.get(self.pos) {
if self.subpos > 0 && self.subpos < result.actions.len() - 1 {
self.subpos += 1;
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))
}
}