From 20ced561378feb2182c2ab90aabd78b8bb87b63d Mon Sep 17 00:00:00 2001 From: imgurbot12 Date: Mon, 24 Jul 2023 21:24:18 -0700 Subject: [PATCH] feat: much improved internal-state mgmt system --- rmenu/src/config.rs | 10 +- rmenu/src/gui.rs | 146 +++++++++++------------ rmenu/src/main.rs | 5 +- rmenu/src/state.rs | 275 +++++++++++++++++++++++++++++++++++--------- 4 files changed, 297 insertions(+), 139 deletions(-) diff --git a/rmenu/src/config.rs b/rmenu/src/config.rs index 80f6722..aa1fef5 100644 --- a/rmenu/src/config.rs +++ b/rmenu/src/config.rs @@ -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, diff --git a/rmenu/src/gui.rs b/rmenu/src/gui.rs index bc37152..50cfe0b 100644 --- a/rmenu/src/gui.rs +++ b/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, mods: &Modifiers, key: &Code) -> bool { } /// main application function/loop -fn App(cx: Scope) -> Element { - let quit = use_state(cx, || false); - let search = use_state(cx, || "".to_string()); +fn App<'a>(cx: Scope) -> 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 = 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() } }) } diff --git a/rmenu/src/main.rs b/rmenu/src/main.rs index 4bcdc3c..2e41343 100644 --- a/rmenu/src/main.rs +++ b/rmenu/src/main.rs @@ -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(()) } diff --git a/rmenu/src/state.rs b/rmenu/src/state.rs index 5cd7e9c..81cc092 100644 --- a/rmenu/src/state.rs +++ b/rmenu/src/state.rs @@ -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, - subpos: &'a UseState, - results: Vec<&'a Entry>, +#[inline] +fn scroll(cx: Scope, 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, +} + +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, + app: &'a App, + results: Vec<&'a Entry>, +} + +impl<'a> AppState<'a> { + /// Spawn new Application State Tracker + pub fn new(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) -> 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)) } }