From 630c0d4f1d5ff79b9a118d8e424e00998bfbc6b7 Mon Sep 17 00:00:00 2001 From: imgurbot12 Date: Wed, 11 Jan 2023 12:45:02 -0700 Subject: [PATCH] feat: further work on egui-eframe implementation --- crates/rmenu-plugin/src/lib.rs | 1 + crates/rmenu/src/config.rs | 21 ++-- crates/rmenu/src/exec.rs | 0 crates/rmenu/src/gui/gui.rs.bak | 193 -------------------------------- crates/rmenu/src/gui/icons.rs | 7 +- crates/rmenu/src/gui/mod.rs | 159 ++++++++++++-------------- crates/rmenu/src/gui/page.rs | 107 ++++++++++++++++++ plugins/drun/src/desktop.rs | 1 + 8 files changed, 198 insertions(+), 291 deletions(-) create mode 100644 crates/rmenu/src/exec.rs delete mode 100644 crates/rmenu/src/gui/gui.rs.bak create mode 100644 crates/rmenu/src/gui/page.rs diff --git a/crates/rmenu-plugin/src/lib.rs b/crates/rmenu-plugin/src/lib.rs index fe42572..be1c685 100644 --- a/crates/rmenu-plugin/src/lib.rs +++ b/crates/rmenu-plugin/src/lib.rs @@ -25,6 +25,7 @@ pub enum Exec { #[cfg_attr(feature = "cache", derive(Serialize, Deserialize))] pub struct Icon { pub name: RString, + pub path: RString, pub data: RVec, } diff --git a/crates/rmenu/src/config.rs b/crates/rmenu/src/config.rs index 51241e5..5347557 100644 --- a/crates/rmenu/src/config.rs +++ b/crates/rmenu/src/config.rs @@ -24,9 +24,11 @@ pub struct PluginConfig { pub struct RMenuConfig { pub terminal: String, pub icon_size: f32, - pub window_width: f32, - pub window_height: f32, - pub result_size: usize, + pub centered: Option, + pub window_pos: Option<[f32; 2]>, + pub window_size: Option<[f32; 2]>, + pub result_size: Option, + pub decorate_window: bool, } #[derive(Serialize, Deserialize)] @@ -41,9 +43,11 @@ impl Default for Config { rmenu: RMenuConfig { terminal: "foot".to_owned(), icon_size: 20.0, - window_width: 500.0, - window_height: 300.0, - result_size: 15, + centered: Some(true), + window_pos: None, + window_size: Some([500.0, 300.0]), + result_size: Some(15), + decorate_window: false, }, plugins: HashMap::new(), } @@ -75,7 +79,10 @@ pub fn load_config(path: Option) -> Config { // write default config to standard location let config = Config::default(); if path.is_none() { - fs::create_dir(get_config_dir()).expect("failed to make config dir"); + let dir = get_config_dir(); + if !dir.exists() { + fs::create_dir(dir).expect("failed to make config dir"); + } let default = toml::to_string(&config).unwrap(); fs::write(fpath, default).expect("failed to write default config"); } diff --git a/crates/rmenu/src/exec.rs b/crates/rmenu/src/exec.rs new file mode 100644 index 0000000..e69de29 diff --git a/crates/rmenu/src/gui/gui.rs.bak b/crates/rmenu/src/gui/gui.rs.bak deleted file mode 100644 index dd612a3..0000000 --- a/crates/rmenu/src/gui/gui.rs.bak +++ /dev/null @@ -1,193 +0,0 @@ -use std::cmp::min; -use std::process::exit; - -use eframe::egui; -use eframe::egui::ScrollArea; - -use super::exec::exec_command; -use super::icons::{background_load, IconCache}; -use super::modules::{Entries, Mode, ModuleSearch, Settings}; - -/* Application */ - -pub struct App { - modules: ModuleSearch, - search: String, - results: Option, - focus: usize, - images: IconCache, -} - -// application class-related functions and utilities -impl App { - pub fn new(modes: Vec, settings: Settings) -> Self { - let modules = ModuleSearch::new(modes, settings).expect("module search failed"); - let mut app = Self { - search: "".to_owned(), - modules, - results: None, - focus: 0, - images: IconCache::new(), - }; - app.search(); - app - } - - fn search(&mut self) { - // assign values - self.focus = 0; - self.results = self.modules.search(&self.search, 0).ok(); - // load icons in background - if let Some(results) = self.results.as_ref() { - background_load(&mut self.images, 20, results); - } - } - - // shift focus based on size of results and scope of valid range - fn shift_focus(&mut self, shift: i32) { - // handle shifts up - if shift < 0 { - let change = shift.abs() as usize; - if change > self.focus { - self.focus = 0; - return; - } - self.focus -= change; - return; - } - // handle shifts down - let max_pos = if let Some(r) = self.results.as_ref() { - r.len() - 1 - } else { - 0 - }; - self.focus = min(self.focus + shift as usize, max_pos); - } -} - -// ui component functions -impl App { - // implement keyboard navigation controls between menu items - #[inline] - fn keyboard_controls(&mut self, ctx: &egui::Context) { - // tab/ctrl+tab controls - if ctx.input().key_pressed(egui::Key::Tab) { - if ctx.input().modifiers.ctrl { - self.shift_focus(-1); - } else { - self.shift_focus(1); - }; - } - // arrow-key controls - if ctx.input().key_pressed(egui::Key::ArrowUp) && self.focus > 0 { - self.shift_focus(-1); - } - if ctx.input().key_pressed(egui::Key::ArrowDown) { - self.shift_focus(1); - } - // pageup/down controls - if ctx.input().key_pressed(egui::Key::PageUp) { - self.shift_focus(-5); - } - if ctx.input().key_pressed(egui::Key::PageDown) { - self.shift_focus(5); - } - // escape - if ctx.input().key_pressed(egui::Key::Escape) { - exit(1); - } - // enter - app selection - if ctx.input().key_pressed(egui::Key::Enter) { - let Some(results) = self.results.as_ref() else { return }; - let Some(entry) = results.get(self.focus) else { return }; - exec_command(&entry.exec, entry.terminal); - exit(0); - } - } - - // implement simple topbar searchbar - #[inline] - fn simple_search(&mut self, ui: &mut egui::Ui) { - let size = ui.available_size(); - ui.horizontal(|ui| { - ui.spacing_mut().text_edit_width = size.x; - let search = ui.text_edit_singleline(&mut self.search); - if search.changed() { - self.search(); - } - }); - } - - // implement simple scrolling grid-based results panel - fn simple_results(&mut self, ui: &mut egui::Ui) { - let focus = self.focus; - ScrollArea::vertical() - .auto_shrink([false; 2]) - .show_viewport(ui, |ui, viewport| { - // calculate top/bottom positions and size of each row - let top_pos = viewport.min.y; - let bottom_pos = viewport.max.y; - let spacing = ui.spacing(); - let row_height = spacing.interact_size.y + spacing.item_spacing.y; - // render results and their related fields - let Some(found) = self.results.as_ref() else { return }; - let results = found.clone(); - egui::Grid::new("results") - .with_row_color(move |row, style| { - if row == focus { - return Some(egui::Rgba::from(style.visuals.faint_bg_color)); - }; - None - }) - .show(ui, |ui| { - let has_icons = results - .iter() - .filter(|r| r.icon.is_some()) - .peekable() - .peek() - .is_some(); - for (n, record) in results.into_iter().enumerate() { - let y = n as f32 * row_height; - // load and render image field - // content is contained within a horizontal to keep - // scroll-pos from updating when icon renderings - // change - if has_icons { - ui.horizontal(|ui| { - // only render images that display within view window - if n == 0 || (y < bottom_pos && y > top_pos) { - if let Some(icon) = record.icon.as_ref() { - if let Ok(image) = self.images.load(icon) { - let size = egui::vec2(20.0, 20.0); - image.show_size(ui, size); - } - } - return; - } - ui.label(""); - }); - } - // render text fields - let label = ui.label(&record.name); - if n == self.focus { - label.scroll_to_me(None) - } - if let Some(extra) = record.comment.as_ref() { - ui.label(extra); - } - ui.end_row(); - } - }); - }); - } -} - -impl eframe::App for App { - fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { - egui::CentralPanel::default().show(ctx, |ui| { - self.keyboard_controls(ctx); - self.simple_search(ui); - self.simple_results(ui); - }); - } -} diff --git a/crates/rmenu/src/gui/icons.rs b/crates/rmenu/src/gui/icons.rs index fa34edb..a692e45 100644 --- a/crates/rmenu/src/gui/icons.rs +++ b/crates/rmenu/src/gui/icons.rs @@ -47,13 +47,14 @@ impl IconCache { // save icon to cache if not already saved pub fn save(&mut self, icon: &Icon) -> Result<(), String> { let name = icon.name.as_str(); + let path = icon.path.as_str(); if !self.c.contains_key(name) { self.c.insert( name.to_owned(), - if name.ends_with(".svg") { - RetainedImage::from_svg_bytes(name, &icon.data)? + if path.ends_with(".svg") { + RetainedImage::from_svg_bytes(path, &icon.data)? } else { - RetainedImage::from_image_bytes(name, &icon.data)? + RetainedImage::from_image_bytes(path, &icon.data)? }, ); } diff --git a/crates/rmenu/src/gui/mod.rs b/crates/rmenu/src/gui/mod.rs index 7623ba7..408e353 100644 --- a/crates/rmenu/src/gui/mod.rs +++ b/crates/rmenu/src/gui/mod.rs @@ -1,22 +1,45 @@ /*! * Rmenu - Egui implementation */ -use std::cmp::min; +use std::process::exit; use eframe::egui; -use rmenu_plugin::Entry; mod icons; +mod page; use icons::{load_images, IconCache}; +use page::Paginator; use crate::{config::Config, plugins::Plugins}; +// v1: +//TODO: fix grid so items expand entire length of window +//TODO: remove prefix and name specification from module definition +//TODO: allow specifying prefix in search to limit enabled plugins +//TODO: build in the actual execute and close part +//TODO: build compilation and install script for easy setup +//TODO: allow for close-on-defocus option in config? + +// v2: +//TODO: look into dynamic rendering w/ a custom style config - maybe even css? +//TODO: add additonal plugins: file-browser, browser-url, etc... + /* Function */ // spawn gui application and run it pub fn launch_gui(cfg: Config, plugins: Plugins) -> Result<(), eframe::Error> { + let pos = match cfg.rmenu.window_pos { + Some(pos) => Some(egui::pos2(pos[0], pos[1])), + None => None, + }; + let size = cfg.rmenu.window_size.unwrap_or([550.0, 350.0]); let options = eframe::NativeOptions { - initial_window_size: Some(egui::vec2(cfg.rmenu.window_width, cfg.rmenu.window_height)), + transparent: true, + always_on_top: true, + decorated: cfg.rmenu.decorate_window, + centered: cfg.rmenu.centered.unwrap_or(false), + initial_window_pos: pos, + initial_window_size: Some(egui::vec2(size[0], size[1])), ..Default::default() }; let gui = GUI::new(cfg, plugins); @@ -28,23 +51,19 @@ pub fn launch_gui(cfg: Config, plugins: Plugins) -> Result<(), eframe::Error> { struct GUI { plugins: Plugins, search: String, - results: Vec, - focus: usize, - focus_updated: bool, images: IconCache, config: Config, + page: Paginator, } impl GUI { pub fn new(config: Config, plugins: Plugins) -> Self { let mut gui = Self { - config, plugins, search: "".to_owned(), - results: vec![], - focus: 0, - focus_updated: false, images: IconCache::new(), + page: Paginator::new(config.rmenu.result_size.clone().unwrap_or(15)), + config, }; // pre-run empty search to generate cache gui.search(); @@ -53,33 +72,11 @@ impl GUI { // complete search based on current internal search variable fn search(&mut self) { - // update variables and complete search - self.focus = 0; - self.results = self.plugins.search(&self.search); - self.focus_updated = true; - // load icons in background - if self.results.len() > 0 { - load_images(&mut self.images, 20, &self.results); + let results = self.plugins.search(&self.search); + if results.len() > 0 { + load_images(&mut self.images, 20, &results); } - } - - #[inline] - fn set_focus(&mut self, focus: usize) { - self.focus = focus; - self.focus_updated = true; - } - - // shift focus up a certain number of rows - #[inline] - fn focus_up(&mut self, shift: usize) { - self.set_focus(self.focus - min(shift, self.focus)); - } - - // shift focus down a certain number of rows - fn focus_down(&mut self, shift: usize) { - let results = self.results.len(); - let max_pos = if results > 0 { results - 1 } else { 0 }; - self.set_focus(min(self.focus + shift, max_pos)); + self.page.reset(results); } #[inline] @@ -87,23 +84,27 @@ impl GUI { // tab / ctrl+tab controls if ctx.input().key_pressed(egui::Key::Tab) { match ctx.input().modifiers.ctrl { - true => self.focus_down(1), - false => self.focus_up(1), + true => self.page.focus_up(1), + false => self.page.focus_down(1), }; } // arrow-key controls if ctx.input().key_pressed(egui::Key::ArrowUp) { - self.focus_up(1); + self.page.focus_up(1); } if ctx.input().key_pressed(egui::Key::ArrowDown) { - self.focus_down(1) + self.page.focus_down(1) } // pageup / pagedown controls if ctx.input().key_pressed(egui::Key::PageUp) { - self.focus_up(5); + self.page.focus_up(5); } if ctx.input().key_pressed(egui::Key::PageDown) { - self.focus_down(5); + self.page.focus_down(5); + } + // exit controls + if ctx.input().key_pressed(egui::Key::Escape) { + exit(1); } } } @@ -115,8 +116,9 @@ impl GUI { let size = ui.available_size(); ui.horizontal(|ui| { ui.spacing_mut().text_edit_width = size.x; - let search = ui.text_edit_singleline(&mut self.search); - if search.changed() { + let search = egui::TextEdit::singleline(&mut self.search).frame(false); + let object = ui.add(search); + if object.changed() { self.search(); } }); @@ -125,7 +127,7 @@ impl GUI { // check if results contain any icons at all #[inline] fn has_icons(&self) -> bool { - self.results + self.page .iter() .filter(|r| r.icon.is_some()) .peekable() @@ -135,7 +137,7 @@ impl GUI { #[inline] fn grid_highlight(&self) -> Box Option> { - let focus = self.focus; + let focus = self.page.row_focus(); Box::new(move |row, style| { if row == focus { return Some(egui::Rgba::from(style.visuals.faint_bg_color)); @@ -147,49 +149,30 @@ impl GUI { // implement simple scrolling grid-based results pannel #[inline] fn simple_results(&mut self, ui: &mut egui::Ui) { - egui::ScrollArea::vertical() - .auto_shrink([false; 2]) - .show_viewport(ui, |ui, viewport| { - // calculate top/bottom positions and size of rows - let spacing = ui.spacing(); - let top_y = viewport.min.y; - let bot_y = viewport.max.y; - let row_h = spacing.interact_size.y + spacing.item_spacing.y; - // render results and their related fields - let results = &self.results; - let has_icons = self.has_icons(); - egui::Grid::new("results") - .with_row_color(self.grid_highlight()) - .show(ui, |ui| { - for (n, record) in results.iter().enumerate() { - // render icon if enabled and within visible bounds - if has_icons { - ui.horizontal(|ui| { - let y = n as f32 * row_h; - if n == 0 || (y < bot_y && y > top_y) { - if let Some(icon) = record.icon.as_ref().into_option() { - if let Ok(image) = self.images.load(&icon) { - let xy = self.config.rmenu.icon_size; - image.show_size(ui, egui::vec2(xy, xy)); - } - } - } - }); + let results = self.page.iter(); + let has_icons = self.has_icons(); + egui::Grid::new("results") + .with_row_color(self.grid_highlight()) + .show(ui, |ui| { + for record in results { + // render icons (if any were present in set) + if has_icons { + ui.horizontal(|ui| { + if let Some(icon) = record.icon.as_ref().into_option() { + if let Ok(image) = self.images.load(&icon) { + let xy = self.config.rmenu.icon_size; + image.show_size(ui, egui::vec2(xy, xy)); + } } - // render main label - let label = ui.label(record.name.as_str()); - // scroll to laebl when focus shifts - if n == self.focus && self.focus_updated { - label.scroll_to_me(None); - self.focus_updated = false; - } - // render comment (if any) - if let Some(comment) = record.comment.as_ref().into_option() { - ui.label(comment.as_str()); - } - ui.end_row(); - } - }); + }); + } + // render content + ui.label(record.name.as_str()); + if let Some(comment) = record.comment.as_ref().into_option() { + ui.label(comment.as_str()); + } + ui.end_row(); + } }); } } diff --git a/crates/rmenu/src/gui/page.rs b/crates/rmenu/src/gui/page.rs new file mode 100644 index 0000000..fe6049f --- /dev/null +++ b/crates/rmenu/src/gui/page.rs @@ -0,0 +1,107 @@ +/* + * Result Paginator Implementation + */ +use std::cmp::min; + +use rmenu_plugin::Entry; + +/// Plugin results paginator implementation +pub struct Paginator { + page: usize, + page_size: usize, + results: Vec, + focus: usize, +} + +impl Paginator { + pub fn new(page_size: usize) -> Self { + Self { + page: 0, + page_size, + results: vec![], + focus: 0, + } + } + + #[inline(always)] + fn lower_bound(&self) -> usize { + self.page * self.page_size + } + + #[inline(always)] + fn upper_bound(&self) -> usize { + (self.page + 1) * self.page_size + } + + fn set_focus(&mut self, focus: usize) { + self.focus = focus; + if self.focus < self.lower_bound() { + self.page -= 1; + } + if self.focus >= self.upper_bound() { + self.page += 1; + } + } + + /// reset paginator location and replace internal results + pub fn reset(&mut self, results: Vec) { + self.page = 0; + self.focus = 0; + self.results = results; + } + + /// calculate zeroed focus based on index in iterator + #[inline] + pub fn row_focus(&self) -> usize { + self.focus - self.lower_bound() + } + + /// shift focus up a certain number of rows + #[inline] + pub fn focus_up(&mut self, shift: usize) { + self.set_focus(self.focus - min(shift, self.focus)); + } + + /// shift focus down a certain number of rows + pub fn focus_down(&mut self, shift: usize) { + let results = self.results.len(); + let max_pos = if results > 0 { results - 1 } else { 0 }; + self.set_focus(min(self.focus + shift, max_pos)); + } + + /// Generate page-size iterator + #[inline] + pub fn iter(&self) -> PageIter { + PageIter::new(self.lower_bound(), self.upper_bound(), &self.results) + } +} + +/// Paginator bounds iterator implementation +pub struct PageIter<'a> { + stop: usize, + cursor: usize, + results: &'a Vec, +} + +impl<'a> PageIter<'a> { + pub fn new(start: usize, stop: usize, results: &'a Vec) -> Self { + Self { + stop, + results, + cursor: start, + } + } +} + +impl<'a> Iterator for PageIter<'a> { + type Item = &'a Entry; + + fn next(&mut self) -> Option { + if self.cursor >= self.stop { + return None; + } + let result = self.results.get(self.cursor); + self.cursor += 1; + result + } +} diff --git a/plugins/drun/src/desktop.rs b/plugins/drun/src/desktop.rs index a69e265..d2975ec 100644 --- a/plugins/drun/src/desktop.rs +++ b/plugins/drun/src/desktop.rs @@ -60,6 +60,7 @@ fn read_icon(name: &str, icons: &Vec) -> Option { let Ok(data) = fs::read(&path) else { return None }; Some(Icon { name: RString::from(name), + path: RString::from(path), data: RVec::from(data), }) }