feat: further work on egui-eframe implementation

This commit is contained in:
imgurbot12 2023-01-11 12:45:02 -07:00
parent 5e114ef36a
commit 630c0d4f1d
8 changed files with 198 additions and 291 deletions

View File

@ -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<u8>,
}

View File

@ -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<bool>,
pub window_pos: Option<[f32; 2]>,
pub window_size: Option<[f32; 2]>,
pub result_size: Option<usize>,
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<String>) -> 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");
}

0
crates/rmenu/src/exec.rs Normal file
View File

View File

@ -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<Entries>,
focus: usize,
images: IconCache,
}
// application class-related functions and utilities
impl App {
pub fn new(modes: Vec<Mode>, 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);
});
}
}

View File

@ -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)?
},
);
}

View File

@ -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<Entry>,
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<dyn Fn(usize, &egui::Style) -> Option<egui::Rgba>> {
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();
}
});
}
}

View File

@ -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<Entry>,
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<Entry>) {
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<Entry>,
}
impl<'a> PageIter<'a> {
pub fn new(start: usize, stop: usize, results: &'a Vec<Entry>) -> Self {
Self {
stop,
results,
cursor: start,
}
}
}
impl<'a> Iterator for PageIter<'a> {
type Item = &'a Entry;
fn next(&mut self) -> Option<Self::Item> {
if self.cursor >= self.stop {
return None;
}
let result = self.results.get(self.cursor);
self.cursor += 1;
result
}
}

View File

@ -60,6 +60,7 @@ fn read_icon(name: &str, icons: &Vec<IconFile>) -> Option<Icon> {
let Ok(data) = fs::read(&path) else { return None };
Some(Icon {
name: RString::from(name),
path: RString::from(path),
data: RVec::from(data),
})
}