mirror of
https://github.com/imgurbot12/rmenu.git
synced 2025-01-12 07:16:21 +01:00
feat: implement custom right-click context menu
This commit is contained in:
parent
d59003a8b0
commit
e19a791023
7 changed files with 234 additions and 85 deletions
|
@ -138,6 +138,9 @@ struct OptionArgs {
|
|||
/// Override Single-Click Activation Option
|
||||
#[arg(long)]
|
||||
pub single_click: Option<bool>,
|
||||
/// Override Right-Click Context-Menu Activation Option
|
||||
#[arg(long)]
|
||||
pub context_menu: Option<bool>,
|
||||
// search settings
|
||||
/// Override Default Placeholder
|
||||
#[arg(short = 'P', long)]
|
||||
|
@ -204,6 +207,7 @@ impl Into<Options> for OptionArgs {
|
|||
placeholder: self.placeholder,
|
||||
hover_select: self.hover_select,
|
||||
single_click: self.single_click,
|
||||
context_menu: self.context_menu,
|
||||
search_restrict: self.search_restrict,
|
||||
search_max_length: self.search_max_length,
|
||||
key_exec: self.key_exec,
|
||||
|
|
|
@ -98,6 +98,8 @@ pub struct Options {
|
|||
pub hover_select: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub single_click: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub context_menu: Option<bool>,
|
||||
// search settings
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub placeholder: Option<String>,
|
||||
|
|
|
@ -104,3 +104,32 @@ img {
|
|||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* Context Menu */
|
||||
|
||||
.context-menu {
|
||||
position: absolute;
|
||||
}
|
||||
.menu {
|
||||
margin: 0;
|
||||
padding: 10px 0;
|
||||
list-style-type: none;
|
||||
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 10px 20px rgb(64 64 64 / 5%);
|
||||
background-color: #fff;
|
||||
}
|
||||
.menu > li > a {
|
||||
font: inherit;
|
||||
display: flex;
|
||||
|
||||
width: -webkit-fill-available;
|
||||
padding: 5px 30px 5px 15px;
|
||||
|
||||
color: #000;
|
||||
font-weight: 500;
|
||||
text-decoration: unset;
|
||||
}
|
||||
.menu > li > a:hover {
|
||||
background-color: lightblue;
|
||||
}
|
||||
|
|
|
@ -85,6 +85,9 @@ pub struct Args {
|
|||
/// Activate Menu Result with Single Click
|
||||
#[arg(long)]
|
||||
single_click: Option<bool>,
|
||||
/// Allow Right Click Context Menu
|
||||
#[arg(long)]
|
||||
context_menu: Option<bool>,
|
||||
|
||||
// search settings
|
||||
/// Enforce Regex Pattern on Search
|
||||
|
@ -226,6 +229,7 @@ impl Args {
|
|||
config.use_comments = self.use_icons.unwrap_or(config.use_comments);
|
||||
config.hover_select = self.hover_select.unwrap_or(config.hover_select);
|
||||
config.single_click = self.single_click.unwrap_or(config.single_click);
|
||||
config.context_menu = self.context_menu.unwrap_or(config.context_menu);
|
||||
// override search settings
|
||||
cfg_replace!(config.search.restrict, self.search_restrict);
|
||||
cfg_replace!(config.search.max_length, self.search_max_length, true);
|
||||
|
|
|
@ -28,6 +28,7 @@ pub struct Config {
|
|||
pub use_comments: bool,
|
||||
pub hover_select: bool,
|
||||
pub single_click: bool,
|
||||
pub context_menu: bool,
|
||||
pub search: SearchConfig,
|
||||
pub window: WindowConfig,
|
||||
pub keybinds: KeyConfig,
|
||||
|
@ -46,6 +47,7 @@ impl Default for Config {
|
|||
use_comments: true,
|
||||
hover_select: false,
|
||||
single_click: false,
|
||||
context_menu: false,
|
||||
search: Default::default(),
|
||||
window: Default::default(),
|
||||
keybinds: Default::default(),
|
||||
|
@ -63,6 +65,7 @@ impl Config {
|
|||
cfg_replace!(self.jump_dist, options.jump_dist, true);
|
||||
cfg_replace!(self.hover_select, options.hover_select, true);
|
||||
cfg_replace!(self.single_click, options.single_click, true);
|
||||
cfg_replace!(self.context_menu, options.context_menu, true);
|
||||
// search settings
|
||||
cfg_replace!(self.search.placeholder, options.placeholder);
|
||||
cfg_replace!(self.search.restrict, options.search_restrict);
|
||||
|
|
|
@ -7,7 +7,7 @@ mod image;
|
|||
mod state;
|
||||
|
||||
pub use state::ContextBuilder;
|
||||
use state::{Context, Position};
|
||||
use state::{Context, ContextMenu, Position};
|
||||
|
||||
const DEFAULT_CSS_CONTENT: &'static str = include_str!("../../public/default.css");
|
||||
|
||||
|
@ -25,15 +25,125 @@ pub fn run(ctx: Context) {
|
|||
.with_theme(ctx.config.window.get_theme());
|
||||
let config = dioxus_desktop::Config::default()
|
||||
.with_window(window)
|
||||
.with_menu(None);
|
||||
.with_menu(None)
|
||||
.with_disable_context_menu(true);
|
||||
LaunchBuilder::desktop()
|
||||
.with_cfg(config)
|
||||
.with_context(Rc::new(RefCell::new(ctx)))
|
||||
.launch(gui_main);
|
||||
}
|
||||
|
||||
fn gui_main() -> Element {
|
||||
// build context and signals for state
|
||||
let ctx = use_context::<Ctx>();
|
||||
let window = dioxus_desktop::use_window();
|
||||
let mut search = use_signal(String::new);
|
||||
let mut position = use_signal(Position::default);
|
||||
let mut results = use_signal(|| ctx.borrow().all_results());
|
||||
let mut ctx_menu = use_signal(ContextMenu::default);
|
||||
|
||||
// refocus on input
|
||||
let js = format!("setTimeout(() => {{ document.getElementById('search').focus() }}, 100)");
|
||||
eval(&js);
|
||||
|
||||
// configure exit cleanup function
|
||||
use_drop(move || {
|
||||
let ctx = consume_context::<Ctx>();
|
||||
ctx.borrow_mut().cleanup();
|
||||
});
|
||||
|
||||
// update search results on search
|
||||
let effect_ctx = use_context::<Ctx>();
|
||||
use_effect(move || {
|
||||
let search = search();
|
||||
results.set(effect_ctx.borrow_mut().set_search(&search, &mut position));
|
||||
});
|
||||
|
||||
// declare keyboard handler
|
||||
let key_ctx = use_context::<Ctx>();
|
||||
let keydown = move |e: KeyboardEvent| {
|
||||
let context = key_ctx.borrow();
|
||||
// suport console key
|
||||
#[cfg(debug_assertions)]
|
||||
if e.code() == Code::Backquote {
|
||||
window.devtool();
|
||||
return;
|
||||
}
|
||||
// calculate current entry index
|
||||
let pos = position.with(|p| p.pos);
|
||||
let index = results.with(|r| r.get(pos).cloned().unwrap_or(0));
|
||||
// handle events
|
||||
let quit = context.handle_keybinds(e, index, &mut position);
|
||||
// if e.code() == Code::
|
||||
// handle quit event
|
||||
if quit {
|
||||
window.set_visible(false);
|
||||
spawn(async move {
|
||||
// wait for window to vanish
|
||||
let time = std::time::Duration::from_millis(50);
|
||||
let window = dioxus_desktop::use_window();
|
||||
while window.is_visible() {
|
||||
tokio::time::sleep(time).await;
|
||||
}
|
||||
// actually close app after it becomes invisible
|
||||
window.close();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
let context = ctx.borrow();
|
||||
let pattern = context.config.search.restrict.clone();
|
||||
let maxlength = context.config.search.max_length as i64;
|
||||
let max_result = context.calc_limit(&position);
|
||||
rsx! {
|
||||
style { "{DEFAULT_CSS_CONTENT}" }
|
||||
style { "{context.theme}" }
|
||||
style { "{context.css}" }
|
||||
// menu content
|
||||
div {
|
||||
id: "content",
|
||||
class: "content",
|
||||
onclick: move |_| {
|
||||
ctx_menu.with_mut(|m| m.reset());
|
||||
},
|
||||
onkeydown: keydown,
|
||||
prevent_default: "contextmenu",
|
||||
div {
|
||||
id: "navbar",
|
||||
class: "navbar",
|
||||
input {
|
||||
id: "search",
|
||||
value: "{search}",
|
||||
pattern: pattern,
|
||||
maxlength: maxlength,
|
||||
oninput: move |e| search.set(e.value()),
|
||||
}
|
||||
}
|
||||
div {
|
||||
id: "results",
|
||||
class: "results",
|
||||
for (pos, index) in results().iter().take(max_result).enumerate() {
|
||||
gui_entry {
|
||||
key: "{pos}-{index}",
|
||||
ctx_menu,
|
||||
position,
|
||||
search_index: pos,
|
||||
entry_index: *index,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// custom context menu
|
||||
context_menu {
|
||||
ctx_menu,
|
||||
position,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Props)]
|
||||
struct Row {
|
||||
ctx_menu: Signal<ContextMenu>,
|
||||
position: Signal<Position>,
|
||||
search_index: usize,
|
||||
entry_index: usize,
|
||||
|
@ -70,6 +180,7 @@ fn gui_entry(mut row: Row) -> Element {
|
|||
let (pos, subpos) = row.position.with(|p| (p.pos, p.subpos));
|
||||
// build element from entry
|
||||
let single_click = context.config.single_click;
|
||||
let context_menu = context.config.context_menu;
|
||||
let action_select = pos == row.search_index && subpos > 0;
|
||||
let aclass = action_select.then_some("active").unwrap_or_default();
|
||||
let rclass = (pos == row.search_index && subpos == 0)
|
||||
|
@ -77,6 +188,7 @@ fn gui_entry(mut row: Row) -> Element {
|
|||
.unwrap_or_default();
|
||||
let result_ctx1 = use_context::<Ctx>();
|
||||
let result_ctx2 = use_context::<Ctx>();
|
||||
let menu_active = row.ctx_menu.with(|m| m.is_active());
|
||||
rsx! {
|
||||
div {
|
||||
class: "result-entry",
|
||||
|
@ -85,21 +197,34 @@ fn gui_entry(mut row: Row) -> Element {
|
|||
id: "result-{row.search_index}",
|
||||
class: "result {rclass}",
|
||||
// actions
|
||||
oncontextmenu: move |e| {
|
||||
if context_menu {
|
||||
let mouse: MouseData = e
|
||||
.downcast::<SerializedMouseData>()
|
||||
.cloned()
|
||||
.unwrap()
|
||||
.into();
|
||||
let coords = mouse.page_coordinates();
|
||||
row.ctx_menu.with_mut(|c| c.set(row.entry_index, coords));
|
||||
}
|
||||
},
|
||||
onmouseenter: move |_| {
|
||||
if hover_select {
|
||||
if hover_select && !menu_active {
|
||||
row.position.with_mut(|p| p.set(row.search_index, 0));
|
||||
}
|
||||
},
|
||||
onclick: move |_| {
|
||||
row.position.with_mut(|p| p.set(row.search_index, 0));
|
||||
if single_click {
|
||||
if single_click && !menu_active {
|
||||
let pos = row.position.clone();
|
||||
result_ctx1.borrow().execute(row.entry_index, &pos);
|
||||
}
|
||||
},
|
||||
ondoubleclick: move |_| {
|
||||
let pos = row.position.clone();
|
||||
result_ctx2.borrow().execute(row.entry_index, &pos);
|
||||
if !menu_active {
|
||||
let pos = row.position.clone();
|
||||
result_ctx2.borrow().execute(row.entry_index, &pos);
|
||||
}
|
||||
},
|
||||
// content
|
||||
if context.config.use_icons {
|
||||
|
@ -180,88 +305,40 @@ fn gui_entry(mut row: Row) -> Element {
|
|||
}
|
||||
}
|
||||
|
||||
fn gui_main() -> Element {
|
||||
// build context and signals for state
|
||||
#[component]
|
||||
fn context_menu(ctx_menu: Signal<ContextMenu>, position: Signal<Position>) -> Element {
|
||||
let ctx = use_context::<Ctx>();
|
||||
let window = dioxus_desktop::use_window();
|
||||
let mut search = use_signal(String::new);
|
||||
let mut position = use_signal(Position::default);
|
||||
let mut results = use_signal(|| ctx.borrow().all_results());
|
||||
|
||||
// refocus on input
|
||||
let js = format!("setTimeout(() => {{ document.getElementById('search').focus() }}, 100)");
|
||||
eval(&js);
|
||||
|
||||
// configure exit cleanup function
|
||||
use_drop(move || {
|
||||
let ctx = consume_context::<Ctx>();
|
||||
ctx.borrow_mut().cleanup();
|
||||
});
|
||||
|
||||
// update search results on search
|
||||
let effect_ctx = use_context::<Ctx>();
|
||||
use_effect(move || {
|
||||
let search = search();
|
||||
results.set(effect_ctx.borrow_mut().set_search(&search, &mut position));
|
||||
});
|
||||
|
||||
// declare keyboard handler
|
||||
let key_ctx = use_context::<Ctx>();
|
||||
let keydown = move |e: KeyboardEvent| {
|
||||
let context = key_ctx.borrow();
|
||||
// calculate current entry index
|
||||
let pos = position.with(|p| p.pos);
|
||||
let index = results.with(|r| r.get(pos).cloned().unwrap_or(0));
|
||||
// handle events
|
||||
let quit = context.handle_keybinds(e, index, &mut position);
|
||||
// handle quit event
|
||||
if quit {
|
||||
window.set_visible(false);
|
||||
spawn(async move {
|
||||
// wait for window to vanish
|
||||
let time = std::time::Duration::from_millis(50);
|
||||
let window = dioxus_desktop::use_window();
|
||||
while window.is_visible() {
|
||||
tokio::time::sleep(time).await;
|
||||
}
|
||||
// actually close app after it becomes invisible
|
||||
window.close();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
let context = ctx.borrow();
|
||||
let pattern = context.config.search.restrict.clone();
|
||||
let maxlength = context.config.search.max_length as i64;
|
||||
let max_result = context.calc_limit(&position);
|
||||
let index = ctx_menu.with(|c| c.entry.unwrap_or(0));
|
||||
let entry = context.get_entry(index);
|
||||
rsx! {
|
||||
style { "{DEFAULT_CSS_CONTENT}" }
|
||||
style { "{context.theme}" }
|
||||
style { "{context.css}" }
|
||||
div {
|
||||
id: "content",
|
||||
class: "content",
|
||||
onkeydown: keydown,
|
||||
div {
|
||||
id: "navbar",
|
||||
class: "navbar",
|
||||
input {
|
||||
id: "search",
|
||||
value: "{search}",
|
||||
pattern: pattern,
|
||||
maxlength: maxlength,
|
||||
oninput: move |e| search.set(e.value()),
|
||||
}
|
||||
}
|
||||
div {
|
||||
id: "results",
|
||||
class: "results",
|
||||
for (pos, index) in results().iter().take(max_result).enumerate() {
|
||||
gui_entry {
|
||||
key: "{pos}-{index}",
|
||||
position,
|
||||
search_index: pos,
|
||||
entry_index: *index,
|
||||
id: "context-menu",
|
||||
class: "context-menu",
|
||||
style: ctx_menu.with(|m| m.style()),
|
||||
ul {
|
||||
class: "menu",
|
||||
for (idx, name, ctx) in entry.actions
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(idx, action)| {
|
||||
let name = match idx == 0 {
|
||||
true => format!("Launch {:?}", entry.name),
|
||||
false => action.name.to_owned(),
|
||||
};
|
||||
(idx, name, use_context::<Ctx>())
|
||||
}) {
|
||||
li {
|
||||
class: "menu-action",
|
||||
a {
|
||||
href: "#",
|
||||
onclick: move |_| {
|
||||
position.with_mut(|p| p.subpos = idx);
|
||||
let pos = position.clone();
|
||||
ctx.borrow().execute(index, &pos);
|
||||
},
|
||||
"{name}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
use dioxus::html::geometry::euclid::Point2D;
|
||||
use dioxus::prelude::*;
|
||||
use rmenu_plugin::Entry;
|
||||
|
||||
|
@ -49,8 +50,37 @@ impl ContextBuilder {
|
|||
}
|
||||
}
|
||||
|
||||
/// Custom ContextMenu Tracker
|
||||
#[derive(Debug, Default)]
|
||||
pub struct ContextMenu {
|
||||
pub entry: Option<usize>,
|
||||
pub x: f64,
|
||||
pub y: f64,
|
||||
}
|
||||
|
||||
impl ContextMenu {
|
||||
#[inline]
|
||||
pub fn is_active(&self) -> bool {
|
||||
self.entry.is_some()
|
||||
}
|
||||
pub fn style(&self) -> String {
|
||||
if self.entry.is_none() {
|
||||
return "display: hidden".to_owned();
|
||||
}
|
||||
return format!("display: block; left: {}px; top: {}px", self.x, self.y);
|
||||
}
|
||||
pub fn set<T>(&mut self, index: usize, coords: Point2D<f64, T>) {
|
||||
self.entry = Some(index);
|
||||
self.x = coords.x;
|
||||
self.y = coords.y;
|
||||
}
|
||||
pub fn reset(&mut self) {
|
||||
self.entry = None;
|
||||
}
|
||||
}
|
||||
|
||||
/// Global Position Tracker
|
||||
#[derive(Default)]
|
||||
#[derive(Debug, Default)]
|
||||
pub struct Position {
|
||||
pub pos: usize,
|
||||
pub subpos: usize,
|
||||
|
|
Loading…
Reference in a new issue