forked from mirrors/rmenu
feat: further work on egui-eframe implementation
This commit is contained in:
parent
5e114ef36a
commit
630c0d4f1d
@ -25,6 +25,7 @@ pub enum Exec {
|
|||||||
#[cfg_attr(feature = "cache", derive(Serialize, Deserialize))]
|
#[cfg_attr(feature = "cache", derive(Serialize, Deserialize))]
|
||||||
pub struct Icon {
|
pub struct Icon {
|
||||||
pub name: RString,
|
pub name: RString,
|
||||||
|
pub path: RString,
|
||||||
pub data: RVec<u8>,
|
pub data: RVec<u8>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -24,9 +24,11 @@ pub struct PluginConfig {
|
|||||||
pub struct RMenuConfig {
|
pub struct RMenuConfig {
|
||||||
pub terminal: String,
|
pub terminal: String,
|
||||||
pub icon_size: f32,
|
pub icon_size: f32,
|
||||||
pub window_width: f32,
|
pub centered: Option<bool>,
|
||||||
pub window_height: f32,
|
pub window_pos: Option<[f32; 2]>,
|
||||||
pub result_size: usize,
|
pub window_size: Option<[f32; 2]>,
|
||||||
|
pub result_size: Option<usize>,
|
||||||
|
pub decorate_window: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
@ -41,9 +43,11 @@ impl Default for Config {
|
|||||||
rmenu: RMenuConfig {
|
rmenu: RMenuConfig {
|
||||||
terminal: "foot".to_owned(),
|
terminal: "foot".to_owned(),
|
||||||
icon_size: 20.0,
|
icon_size: 20.0,
|
||||||
window_width: 500.0,
|
centered: Some(true),
|
||||||
window_height: 300.0,
|
window_pos: None,
|
||||||
result_size: 15,
|
window_size: Some([500.0, 300.0]),
|
||||||
|
result_size: Some(15),
|
||||||
|
decorate_window: false,
|
||||||
},
|
},
|
||||||
plugins: HashMap::new(),
|
plugins: HashMap::new(),
|
||||||
}
|
}
|
||||||
@ -75,7 +79,10 @@ pub fn load_config(path: Option<String>) -> Config {
|
|||||||
// write default config to standard location
|
// write default config to standard location
|
||||||
let config = Config::default();
|
let config = Config::default();
|
||||||
if path.is_none() {
|
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();
|
let default = toml::to_string(&config).unwrap();
|
||||||
fs::write(fpath, default).expect("failed to write default config");
|
fs::write(fpath, default).expect("failed to write default config");
|
||||||
}
|
}
|
||||||
|
0
crates/rmenu/src/exec.rs
Normal file
0
crates/rmenu/src/exec.rs
Normal 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);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
@ -47,13 +47,14 @@ impl IconCache {
|
|||||||
// save icon to cache if not already saved
|
// save icon to cache if not already saved
|
||||||
pub fn save(&mut self, icon: &Icon) -> Result<(), String> {
|
pub fn save(&mut self, icon: &Icon) -> Result<(), String> {
|
||||||
let name = icon.name.as_str();
|
let name = icon.name.as_str();
|
||||||
|
let path = icon.path.as_str();
|
||||||
if !self.c.contains_key(name) {
|
if !self.c.contains_key(name) {
|
||||||
self.c.insert(
|
self.c.insert(
|
||||||
name.to_owned(),
|
name.to_owned(),
|
||||||
if name.ends_with(".svg") {
|
if path.ends_with(".svg") {
|
||||||
RetainedImage::from_svg_bytes(name, &icon.data)?
|
RetainedImage::from_svg_bytes(path, &icon.data)?
|
||||||
} else {
|
} else {
|
||||||
RetainedImage::from_image_bytes(name, &icon.data)?
|
RetainedImage::from_image_bytes(path, &icon.data)?
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,22 +1,45 @@
|
|||||||
/*!
|
/*!
|
||||||
* Rmenu - Egui implementation
|
* Rmenu - Egui implementation
|
||||||
*/
|
*/
|
||||||
use std::cmp::min;
|
use std::process::exit;
|
||||||
|
|
||||||
use eframe::egui;
|
use eframe::egui;
|
||||||
use rmenu_plugin::Entry;
|
|
||||||
|
|
||||||
mod icons;
|
mod icons;
|
||||||
|
mod page;
|
||||||
use icons::{load_images, IconCache};
|
use icons::{load_images, IconCache};
|
||||||
|
use page::Paginator;
|
||||||
|
|
||||||
use crate::{config::Config, plugins::Plugins};
|
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 */
|
/* Function */
|
||||||
|
|
||||||
// spawn gui application and run it
|
// spawn gui application and run it
|
||||||
pub fn launch_gui(cfg: Config, plugins: Plugins) -> Result<(), eframe::Error> {
|
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 {
|
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()
|
..Default::default()
|
||||||
};
|
};
|
||||||
let gui = GUI::new(cfg, plugins);
|
let gui = GUI::new(cfg, plugins);
|
||||||
@ -28,23 +51,19 @@ pub fn launch_gui(cfg: Config, plugins: Plugins) -> Result<(), eframe::Error> {
|
|||||||
struct GUI {
|
struct GUI {
|
||||||
plugins: Plugins,
|
plugins: Plugins,
|
||||||
search: String,
|
search: String,
|
||||||
results: Vec<Entry>,
|
|
||||||
focus: usize,
|
|
||||||
focus_updated: bool,
|
|
||||||
images: IconCache,
|
images: IconCache,
|
||||||
config: Config,
|
config: Config,
|
||||||
|
page: Paginator,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl GUI {
|
impl GUI {
|
||||||
pub fn new(config: Config, plugins: Plugins) -> Self {
|
pub fn new(config: Config, plugins: Plugins) -> Self {
|
||||||
let mut gui = Self {
|
let mut gui = Self {
|
||||||
config,
|
|
||||||
plugins,
|
plugins,
|
||||||
search: "".to_owned(),
|
search: "".to_owned(),
|
||||||
results: vec![],
|
|
||||||
focus: 0,
|
|
||||||
focus_updated: false,
|
|
||||||
images: IconCache::new(),
|
images: IconCache::new(),
|
||||||
|
page: Paginator::new(config.rmenu.result_size.clone().unwrap_or(15)),
|
||||||
|
config,
|
||||||
};
|
};
|
||||||
// pre-run empty search to generate cache
|
// pre-run empty search to generate cache
|
||||||
gui.search();
|
gui.search();
|
||||||
@ -53,33 +72,11 @@ impl GUI {
|
|||||||
|
|
||||||
// complete search based on current internal search variable
|
// complete search based on current internal search variable
|
||||||
fn search(&mut self) {
|
fn search(&mut self) {
|
||||||
// update variables and complete search
|
let results = self.plugins.search(&self.search);
|
||||||
self.focus = 0;
|
if results.len() > 0 {
|
||||||
self.results = self.plugins.search(&self.search);
|
load_images(&mut self.images, 20, &results);
|
||||||
self.focus_updated = true;
|
|
||||||
// load icons in background
|
|
||||||
if self.results.len() > 0 {
|
|
||||||
load_images(&mut self.images, 20, &self.results);
|
|
||||||
}
|
}
|
||||||
}
|
self.page.reset(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));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
@ -87,23 +84,27 @@ impl GUI {
|
|||||||
// tab / ctrl+tab controls
|
// tab / ctrl+tab controls
|
||||||
if ctx.input().key_pressed(egui::Key::Tab) {
|
if ctx.input().key_pressed(egui::Key::Tab) {
|
||||||
match ctx.input().modifiers.ctrl {
|
match ctx.input().modifiers.ctrl {
|
||||||
true => self.focus_down(1),
|
true => self.page.focus_up(1),
|
||||||
false => self.focus_up(1),
|
false => self.page.focus_down(1),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
// arrow-key controls
|
// arrow-key controls
|
||||||
if ctx.input().key_pressed(egui::Key::ArrowUp) {
|
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) {
|
if ctx.input().key_pressed(egui::Key::ArrowDown) {
|
||||||
self.focus_down(1)
|
self.page.focus_down(1)
|
||||||
}
|
}
|
||||||
// pageup / pagedown controls
|
// pageup / pagedown controls
|
||||||
if ctx.input().key_pressed(egui::Key::PageUp) {
|
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) {
|
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();
|
let size = ui.available_size();
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
ui.spacing_mut().text_edit_width = size.x;
|
ui.spacing_mut().text_edit_width = size.x;
|
||||||
let search = ui.text_edit_singleline(&mut self.search);
|
let search = egui::TextEdit::singleline(&mut self.search).frame(false);
|
||||||
if search.changed() {
|
let object = ui.add(search);
|
||||||
|
if object.changed() {
|
||||||
self.search();
|
self.search();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -125,7 +127,7 @@ impl GUI {
|
|||||||
// check if results contain any icons at all
|
// check if results contain any icons at all
|
||||||
#[inline]
|
#[inline]
|
||||||
fn has_icons(&self) -> bool {
|
fn has_icons(&self) -> bool {
|
||||||
self.results
|
self.page
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|r| r.icon.is_some())
|
.filter(|r| r.icon.is_some())
|
||||||
.peekable()
|
.peekable()
|
||||||
@ -135,7 +137,7 @@ impl GUI {
|
|||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn grid_highlight(&self) -> Box<dyn Fn(usize, &egui::Style) -> Option<egui::Rgba>> {
|
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| {
|
Box::new(move |row, style| {
|
||||||
if row == focus {
|
if row == focus {
|
||||||
return Some(egui::Rgba::from(style.visuals.faint_bg_color));
|
return Some(egui::Rgba::from(style.visuals.faint_bg_color));
|
||||||
@ -147,50 +149,31 @@ impl GUI {
|
|||||||
// implement simple scrolling grid-based results pannel
|
// implement simple scrolling grid-based results pannel
|
||||||
#[inline]
|
#[inline]
|
||||||
fn simple_results(&mut self, ui: &mut egui::Ui) {
|
fn simple_results(&mut self, ui: &mut egui::Ui) {
|
||||||
egui::ScrollArea::vertical()
|
let results = self.page.iter();
|
||||||
.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();
|
let has_icons = self.has_icons();
|
||||||
egui::Grid::new("results")
|
egui::Grid::new("results")
|
||||||
.with_row_color(self.grid_highlight())
|
.with_row_color(self.grid_highlight())
|
||||||
.show(ui, |ui| {
|
.show(ui, |ui| {
|
||||||
for (n, record) in results.iter().enumerate() {
|
for record in results {
|
||||||
// render icon if enabled and within visible bounds
|
// render icons (if any were present in set)
|
||||||
if has_icons {
|
if has_icons {
|
||||||
ui.horizontal(|ui| {
|
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 Some(icon) = record.icon.as_ref().into_option() {
|
||||||
if let Ok(image) = self.images.load(&icon) {
|
if let Ok(image) = self.images.load(&icon) {
|
||||||
let xy = self.config.rmenu.icon_size;
|
let xy = self.config.rmenu.icon_size;
|
||||||
image.show_size(ui, egui::vec2(xy, xy));
|
image.show_size(ui, egui::vec2(xy, xy));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// render main label
|
// render content
|
||||||
let label = ui.label(record.name.as_str());
|
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() {
|
if let Some(comment) = record.comment.as_ref().into_option() {
|
||||||
ui.label(comment.as_str());
|
ui.label(comment.as_str());
|
||||||
}
|
}
|
||||||
ui.end_row();
|
ui.end_row();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
107
crates/rmenu/src/gui/page.rs
Normal file
107
crates/rmenu/src/gui/page.rs
Normal 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
|
||||||
|
}
|
||||||
|
}
|
@ -60,6 +60,7 @@ fn read_icon(name: &str, icons: &Vec<IconFile>) -> Option<Icon> {
|
|||||||
let Ok(data) = fs::read(&path) else { return None };
|
let Ok(data) = fs::read(&path) else { return None };
|
||||||
Some(Icon {
|
Some(Icon {
|
||||||
name: RString::from(name),
|
name: RString::from(name),
|
||||||
|
path: RString::from(path),
|
||||||
data: RVec::from(data),
|
data: RVec::from(data),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user