From 7b2741c4c17f618b0a5f4201b9d665b2996471bd Mon Sep 17 00:00:00 2001 From: imgurbot12 Date: Fri, 15 Dec 2023 01:34:28 -0700 Subject: [PATCH] feat: submenu, tranparency-support, better cfgs --- rmenu/Cargo.toml | 2 + rmenu/src/config.rs | 72 +++++++++++++++++++------ rmenu/src/gui.rs | 102 +++++++++++++++++++++++++++++------ rmenu/src/main.rs | 22 +------- rmenu/templates/results.html | 25 ++++----- rmenu/test.css | 4 ++ rmenu/web/index.css | 3 +- rmenu/web/index.js | 40 ++++++++++---- themes/launchpad.css | 5 +- 9 files changed, 199 insertions(+), 76 deletions(-) create mode 100644 rmenu/test.css diff --git a/rmenu/Cargo.toml b/rmenu/Cargo.toml index 92028cc..2696f19 100644 --- a/rmenu/Cargo.toml +++ b/rmenu/Cargo.toml @@ -10,6 +10,8 @@ askama = "0.12.1" base64 = "0.21.5" clap = { version = "4.4.11", features = ["derive"] } env_logger = "0.10.1" +gdk-sys = "0.18.0" +gtk-sys = "0.18.0" heck = "0.4.1" keyboard-types = "0.7.0" lastlog = { version = "0.2.3", features = ["libc"] } diff --git a/rmenu/src/config.rs b/rmenu/src/config.rs index 806f98e..4245d99 100644 --- a/rmenu/src/config.rs +++ b/rmenu/src/config.rs @@ -7,6 +7,16 @@ use keyboard_types::{Code, Modifiers}; use rmenu_plugin::Options; use serde::{de::Error, Deserialize}; +#[inline(always)] +fn _true() -> bool { + true +} + +#[inline(always)] +fn _false() -> bool { + false +} + // parse supported modifiers from string fn mod_from_str(s: &str) -> Option { match s.to_lowercase().as_str() { @@ -95,8 +105,8 @@ impl Default for KeyConfig { exit: vec![Keybind::new(Code::Escape)], move_next: vec![Keybind::new(Code::ArrowDown)], move_prev: vec![Keybind::new(Code::ArrowUp)], - open_menu: vec![], - close_menu: vec![], + open_menu: vec![Keybind::new(Code::ArrowRight)], + close_menu: vec![Keybind::new(Code::ArrowLeft)], jump_next: vec![Keybind::new(Code::PageDown)], jump_prev: vec![Keybind::new(Code::PageUp)], }; @@ -109,22 +119,36 @@ pub struct LogicalSize { pub height: T, } -impl LogicalSize { - pub fn new(width: T, height: T) -> Self { - Self { width, height } +impl Default for LogicalSize { + fn default() -> Self { + Self { + width: 800.0, + height: 400.0, + } } } +#[inline(always)] +fn _title() -> String { + "RMenu Application Launcher".to_owned() +} + /// GUI Desktop Window Configuration Settings #[derive(Debug, PartialEq, Deserialize)] pub struct WindowConfig { + #[serde(default = "_title")] pub title: String, + #[serde(default = "LogicalSize::default")] pub size: LogicalSize, #[serde(default = "_true")] pub focus: bool, + #[serde(default = "_false")] pub decorate: bool, + #[serde(default = "_false")] pub transparent: bool, #[serde(default = "_true")] + pub resizable: bool, + #[serde(default = "_true")] pub always_top: bool, pub fullscreen: Option, pub dark_mode: Option, @@ -133,11 +157,12 @@ pub struct WindowConfig { impl Default for WindowConfig { fn default() -> Self { Self { - title: "RMenu - App Launcher".to_owned(), - size: LogicalSize::new(700.0, 400.0), + title: _title(), + size: LogicalSize::default(), focus: true, decorate: false, transparent: false, + resizable: true, always_top: true, fullscreen: None, dark_mode: None, @@ -199,11 +224,6 @@ pub struct PluginConfig { pub options: Option, } -#[inline] -fn _true() -> bool { - true -} - #[derive(Debug, PartialEq, Deserialize)] #[serde(default)] pub struct SearchConfig { @@ -230,20 +250,42 @@ impl Default for SearchConfig { } } +#[inline(always)] +fn _page_size() -> usize { + 50 +} + +#[inline(always)] +fn _page_load() -> f64 { + 0.8 +} + +#[inline(always)] +fn _jump_dist() -> usize { + 5 +} + /// Global RMenu Complete Configuration #[derive(Debug, PartialEq, Deserialize)] #[serde(default)] pub struct Config { + #[serde(default = "_page_size")] pub page_size: usize, + #[serde(default = "_page_load")] pub page_load: f64, + #[serde(default = "_jump_dist")] pub jump_dist: usize, #[serde(default = "_true")] pub use_icons: bool, #[serde(default = "_true")] pub use_comments: bool, + #[serde(default)] pub search: SearchConfig, + #[serde(default)] pub plugins: BTreeMap, + #[serde(default)] pub keybinds: KeyConfig, + #[serde(default)] pub window: WindowConfig, pub css: Option, pub terminal: Option, @@ -252,9 +294,9 @@ pub struct Config { impl Default for Config { fn default() -> Self { Self { - page_size: 50, - page_load: 0.8, - jump_dist: 5, + page_size: _page_size(), + page_load: _page_load(), + jump_dist: _jump_dist(), use_icons: true, use_comments: true, search: Default::default(), diff --git a/rmenu/src/gui.rs b/rmenu/src/gui.rs index f6bf111..582d5e2 100644 --- a/rmenu/src/gui.rs +++ b/rmenu/src/gui.rs @@ -173,31 +173,74 @@ impl<'a> AppState<'a> { None } - #[inline] + /// Move Up a Number of Full Positions fn move_up(&mut self, up: usize) -> Option { self.pos = std::cmp::max(self.pos, up) - up; + self.subpos = 0; Some(format!("setpos({})", self.pos)) } - #[inline] + /// Move Down a Number of Full Positions fn move_down(&mut self, down: usize) -> Option { let max = (self.page + 1) * self.data.config.page_size; let n = std::cmp::max(self.results.len(), 1); let end = std::cmp::min(max, n) - 1; self.pos = std::cmp::min(self.pos + down, end); + self.subpos = 0; match self.append_results(false) { Some(operation) => Some(operation), None => Some(format!("setpos({})", self.pos)), } } + /// Move Next w/ Context of Sub-Menus + fn move_next(&mut self) -> Option { + if let Some(entry) = self.results.get(self.pos) { + if self.subpos > 0 && self.subpos < entry.actions.len() - 1 { + self.subpos += 1; + return Some(format!("subpos({}, {})", self.pos, self.subpos)); + } + } + self.move_down(1) + } + + /// Move Previous w/ Context of Sub-Menus + fn move_prev(&mut self) -> Option { + if self.subpos > 1 { + self.subpos -= 1; + return Some(format!("subpos({}, {})", self.pos, self.subpos)); + } + if self.subpos == 1 { + self.subpos = 0; + return Some(format!("setpos({})", self.pos)); + } + self.move_up(1) + } + + /// Move Position to Submenu (if one Exists) + fn open_menu(&mut self) -> Option { + if let Some(result) = self.results.get(self.pos) { + let newpos = self.subpos + 1; + if result.actions.len() > newpos { + self.subpos = newpos; + return Some(format!("subpos({}, {})", self.pos, self.subpos)); + } + } + None + } + + /// Close SubMenu (if one is Open) + fn close_menu(&mut self) -> Option { + self.subpos = 0; + Some(format!("setpos({})", self.pos)) + } + /// Handle Search Event sent by UI fn search_event(&mut self, search: String) -> Option { let results = self.search(search); Some(format!("update({results:?})")) } - //TODO: add submenu access and selection //TODO: put back main to reference actual config //TODO: update sway config to make borderless @@ -212,15 +255,13 @@ impl<'a> AppState<'a> { } else if matches(&keybinds.exit, &mods, &code) { std::process::exit(0); } else if matches(&keybinds.move_next, &mods, &code) { - self.move_down(1) + self.move_next() } else if matches(&keybinds.move_prev, &mods, &code) { - self.move_up(1) + self.move_prev() } else if matches(&keybinds.open_menu, &mods, &code) { - // k_updater.set_event(KeyEvent::OpenMenu); - None + self.open_menu() } else if matches(&keybinds.close_menu, &mods, &code) { - // k_updater.set_event(KeyEvent::CloseMenu); - None + self.close_menu() } else if matches(&keybinds.jump_next, &mods, &code) { self.move_down(self.data.config.jump_dist) } else if matches(&keybinds.jump_prev, &mods, &code) { @@ -303,20 +344,41 @@ impl<'a> AppState<'a> { } } +/// Update Gtk's Screen w/ Custom CSS to make Transparent +fn transparency_hack() { + use gdk_sys::gdk_screen_get_default; + use gtk_sys::*; + use std::ffi::CString; + // generate css-provider + let provider = unsafe { gtk_css_provider_new() }; + // apply css to css-provider + let css = CString::new("* { background: transparent }").unwrap(); + let clen = css.as_bytes().len(); + let mut error = std::ptr::null_mut(); + unsafe { gtk_css_provider_load_from_data(provider, css.as_ptr() as _, clen as _, &mut error) }; + // retrieve screen and apply css- provider to screen + let prio = GTK_STYLE_PROVIDER_PRIORITY_APPLICATION; + let screen = unsafe { gdk_screen_get_default() }; + unsafe { gtk_style_context_add_provider_for_screen(screen, provider as _, prio as _) }; +} + /// Run GUI Applcation via WebView pub fn run(data: AppData) { // build app-state let mut state = AppState::new(&data); let html = state.render_index(); // spawn webview instance - let size = &state.data.config.window.size; - web_view::builder() + let wcfg = &state.data.config.window; + let size = &wcfg.size; + let fullscreen = wcfg.fullscreen; + let transparent = wcfg.transparent; + let mut window = web_view::builder() .title(&state.data.config.window.title) .content(Content::Html(html)) - .frameless(!state.data.config.window.decorate) + .frameless(!wcfg.decorate) .size(size.width as i32, size.height as i32) - .resizable(false) - .debug(true) + .resizable(wcfg.resizable) + .debug(cfg!(debug_assertions)) .user_data(()) .invoke_handler(|webview, msg| { if let Some(js) = state.handle_event(msg) { @@ -324,6 +386,16 @@ pub fn run(data: AppData) { }; Ok(()) }) - .run() + .build() .unwrap(); + // manage transparency and fullscreen settings + if transparent { + window.set_color((0, 0, 0, 0)); + #[cfg(target_os = "linux")] + transparency_hack(); + } + if let Some(fullscreen) = fullscreen { + window.set_fullscreen(fullscreen); + } + window.run().unwrap(); } diff --git a/rmenu/src/main.rs b/rmenu/src/main.rs index 1c3c52f..9b18174 100644 --- a/rmenu/src/main.rs +++ b/rmenu/src/main.rs @@ -7,7 +7,6 @@ mod icons; mod search; use clap::Parser; -use config::{CacheSetting, PluginConfig}; use rmenu_plugin::{self_exe, Entry}; static CONFIG_DIR: &'static str = "~/.config/rmenu/"; @@ -42,26 +41,7 @@ fn main() -> cli::Result<()> { // parse cli and retrieve values for app let mut cli = cli::Args::parse(); - // let mut config = cli.get_config()?; - let mut config = crate::config::Config::default(); - config.plugins.insert( - "run".to_owned(), - PluginConfig { - exec: vec!["/home/andrew/.config/rmenu/rmenu-run".to_owned()], - cache: CacheSetting::OnLogin, - placeholder: None, - options: None, - }, - ); - config.plugins.insert( - "drun".to_owned(), - PluginConfig { - exec: vec!["/home/andrew/.config/rmenu/rmenu-desktop".to_owned()], - cache: CacheSetting::OnLogin, - placeholder: None, - options: None, - }, - ); + let mut config = cli.get_config()?; let entries = cli.get_entries(&mut config)?; let css = cli.get_css(&config); let theme = cli.get_theme(); diff --git a/rmenu/templates/results.html b/rmenu/templates/results.html index 28be40e..fba326d 100644 --- a/rmenu/templates/results.html +++ b/rmenu/templates/results.html @@ -31,18 +31,19 @@ {%else%}
{{ entry.name|safe }}
{%endif%} -
- {%for action in entry.actions%} -
-
{{ action.name|safe }}
-
- {%- if let Some(comment) = action.comment %} - {{ comment|safe }} - {%endif%} -
-
- {%endfor%} -
+
+ {%for n in 1..entry.actions.len() %} + {% let action = entry.actions[n] %} +
+
{{ action.name|safe }}
+
+ {%- if let Some(comment) = action.comment %} + {{ comment|safe }} + {%endif%} +
+
+ {%endfor%} +
{%endfor%} diff --git a/rmenu/test.css b/rmenu/test.css new file mode 100644 index 0000000..d124811 --- /dev/null +++ b/rmenu/test.css @@ -0,0 +1,4 @@ + +* { + background: transparent; +} diff --git a/rmenu/web/index.css b/rmenu/web/index.css index 65c8bc0..3503c2e 100644 --- a/rmenu/web/index.css +++ b/rmenu/web/index.css @@ -56,7 +56,8 @@ input { justify-content: left; } -.result > div, .action > div { +.result div, +.action div { margin: 2px 5px; } diff --git a/rmenu/web/index.js b/rmenu/web/index.js index 78105b1..a136dbf 100644 --- a/rmenu/web/index.js +++ b/rmenu/web/index.js @@ -2,6 +2,7 @@ /* Variables */ +const input = document.getElementById("search"); const results = document.getElementById("results"); /* Functions */ @@ -24,8 +25,9 @@ function search(value) { } /// send keydown event back to rust -function keydown({ key, ctrlKey, shiftKey }) { - _send("keydown", { key, "ctrl": ctrlKey, "shift": shiftKey }); +function keydown(e) { + (e.key == "ArrowUp" || e.key == "ArrowDown") && e.preventDefault(); + _send("keydown", { "key": e.key, "ctrl": e.ctrlKey, "shift": e.shiftKey }); } /// send click event back to rust @@ -44,17 +46,22 @@ function scroll() { _send("scroll", { "y": results.scrollTop, "maxy": height }); } +// remove active class from all current objects +function reset() { + const classes = ["active", "selected"]; + for (const cname of classes) { + const selected = document.getElementsByClassName(cname); + const elems = Array.from(selected); + elems.forEach((e) => e.classList.remove(cname)); + } +} + /// set selected-result position function setpos(pos, smooth = false) { - // remove selected class from all current objects - const selected = document.getElementsByClassName("selected"); - const elems = Array.from(selected); - elems.forEach((e) => e.classList.remove("selected")); + reset(); // add selected to current position - let current = document.getElementById(`result-${pos}`); - if (!current) { - return; - } + const current = document.getElementById(`result-${pos}`); + if (!current) return; current.classList.add("selected"); // ensure selected always within view current.scrollIntoView({ @@ -64,6 +71,19 @@ function setpos(pos, smooth = false) { }); } +// set selected-result subposition +function subpos(pos, subpos) { + reset(); + // activate submenu + const actions = document.getElementById(`result-${pos}-actions`); + if (!actions) return; + actions.classList.add("active"); + // select current subposition + const action = document.getElementById(`result-${pos}-action-${subpos}`); + if (!action) return; + action.classList.add("selected"); +} + /// Update Results HTML function update(html) { results.innerHTML = html; diff --git a/themes/launchpad.css b/themes/launchpad.css index d7d4774..8e054ef 100644 --- a/themes/launchpad.css +++ b/themes/launchpad.css @@ -15,7 +15,8 @@ } html, body { - background-color: #24242480; + /* background-color: #383c4a2b !important; */ + background-color: #24242480 !important; } input { @@ -26,7 +27,7 @@ input { border: 1px; border-color: #f5f5f540; border-radius: 2px; - background-color: #363636; + background-color: #363636 !important; } input::placeholder {