mirror of
https://github.com/imgurbot12/rmenu.git
synced 2024-11-10 11:33:48 +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 {
|
||||
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,
|
||||
|
146
rmenu/src/gui.rs
146
rmenu/src/gui.rs
@ -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()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -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(())
|
||||
}
|
||||
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user