mirror of
https://github.com/imgurbot12/rmenu.git
synced 2024-11-10 11:33:48 +01:00
feat: impl keybindings, window-settings, action-execution
This commit is contained in:
parent
9b8d626c4d
commit
31989d4ee8
@ -10,6 +10,7 @@ clap = { version = "4.3.15", features = ["derive"] }
|
|||||||
dioxus = "0.3.2"
|
dioxus = "0.3.2"
|
||||||
dioxus-desktop = "0.3.0"
|
dioxus-desktop = "0.3.0"
|
||||||
dirs = "5.0.1"
|
dirs = "5.0.1"
|
||||||
|
heck = "0.4.1"
|
||||||
keyboard-types = "0.6.2"
|
keyboard-types = "0.6.2"
|
||||||
log = "0.4.19"
|
log = "0.4.19"
|
||||||
regex = { version = "1.9.1", features = ["pattern"] }
|
regex = { version = "1.9.1", features = ["pattern"] }
|
||||||
@ -17,4 +18,6 @@ rmenu-plugin = { version = "0.0.0", path = "../rmenu-plugin" }
|
|||||||
serde = { version = "1.0.171", features = ["derive"] }
|
serde = { version = "1.0.171", features = ["derive"] }
|
||||||
serde_json = "1.0.103"
|
serde_json = "1.0.103"
|
||||||
serde_yaml = "0.9.24"
|
serde_yaml = "0.9.24"
|
||||||
|
shell-words = "1.1.0"
|
||||||
|
shellexpand = "3.1.0"
|
||||||
thiserror = "1.0.43"
|
thiserror = "1.0.43"
|
||||||
|
@ -1,13 +1,143 @@
|
|||||||
use serde::{Deserialize, Serialize};
|
//! RMENU Configuration Implementations
|
||||||
use std::collections::{BTreeMap, VecDeque};
|
use heck::AsPascalCase;
|
||||||
|
use keyboard_types::{Code, Modifiers};
|
||||||
|
use serde::{de::Error, Deserialize};
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Serialize, Deserialize)]
|
use dioxus_desktop::tao::dpi::{LogicalPosition, LogicalSize};
|
||||||
|
|
||||||
|
// parse supported modifiers from string
|
||||||
|
fn mod_from_str(s: &str) -> Option<Modifiers> {
|
||||||
|
match s.to_lowercase().as_str() {
|
||||||
|
"alt" => Some(Modifiers::ALT),
|
||||||
|
"ctrl" => Some(Modifiers::CONTROL),
|
||||||
|
"shift" => Some(Modifiers::SHIFT),
|
||||||
|
"super" => Some(Modifiers::SUPER),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq)]
|
||||||
|
pub struct Keybind {
|
||||||
|
pub mods: Modifiers,
|
||||||
|
pub key: Code,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Keybind {
|
||||||
|
fn new(key: Code) -> Self {
|
||||||
|
Self {
|
||||||
|
mods: Modifiers::empty(),
|
||||||
|
key,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for Keybind {
|
||||||
|
type Err = String;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
// parse modifiers/keys from string
|
||||||
|
let mut mods = vec![];
|
||||||
|
let mut keys = vec![];
|
||||||
|
for item in s.split("+") {
|
||||||
|
let camel = format!("{}", AsPascalCase(item));
|
||||||
|
match Code::from_str(&camel) {
|
||||||
|
Ok(key) => keys.push(key),
|
||||||
|
Err(_) => match mod_from_str(item) {
|
||||||
|
Some(keymod) => mods.push(keymod),
|
||||||
|
None => return Err(format!("invalid key/modifier: {item}")),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// generate final keybind
|
||||||
|
let kmod = mods.into_iter().fold(Modifiers::empty(), |m1, m2| m1 | m2);
|
||||||
|
match keys.len() {
|
||||||
|
0 => Err(format!("no keys specified")),
|
||||||
|
1 => Ok(Keybind {
|
||||||
|
mods: kmod,
|
||||||
|
key: keys.pop().unwrap(),
|
||||||
|
}),
|
||||||
|
_ => Err(format!("too many keys: {keys:?}")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'de> Deserialize<'de> for Keybind {
|
||||||
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||||
|
where
|
||||||
|
D: serde::Deserializer<'de>,
|
||||||
|
{
|
||||||
|
let s: &str = Deserialize::deserialize(deserializer)?;
|
||||||
|
Keybind::from_str(s).map_err(D::Error::custom)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Deserialize)]
|
||||||
|
pub struct KeyConfig {
|
||||||
|
pub exit: Vec<Keybind>,
|
||||||
|
pub move_up: Vec<Keybind>,
|
||||||
|
pub move_down: Vec<Keybind>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub open_menu: Vec<Keybind>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub close_menu: Vec<Keybind>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for KeyConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
return Self {
|
||||||
|
exit: vec![Keybind::new(Code::Escape)],
|
||||||
|
move_up: vec![Keybind::new(Code::ArrowUp)],
|
||||||
|
move_down: vec![Keybind::new(Code::ArrowDown)],
|
||||||
|
open_menu: vec![],
|
||||||
|
close_menu: vec![],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Deserialize)]
|
||||||
|
pub struct WindowConfig {
|
||||||
|
pub title: String,
|
||||||
|
pub size: LogicalSize<f64>,
|
||||||
|
pub position: LogicalPosition<f64>,
|
||||||
|
pub focus: bool,
|
||||||
|
pub decorate: bool,
|
||||||
|
pub transparent: bool,
|
||||||
|
pub always_top: bool,
|
||||||
|
pub dark_mode: Option<bool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for WindowConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
title: "RMenu - App Launcher".to_owned(),
|
||||||
|
size: LogicalSize {
|
||||||
|
width: 700.0,
|
||||||
|
height: 400.0,
|
||||||
|
},
|
||||||
|
position: LogicalPosition { x: 100.0, y: 100.0 },
|
||||||
|
focus: true,
|
||||||
|
decorate: false,
|
||||||
|
transparent: false,
|
||||||
|
always_top: true,
|
||||||
|
dark_mode: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Deserialize)]
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
pub css: Vec<String>,
|
pub css: Vec<String>,
|
||||||
pub use_icons: bool,
|
pub use_icons: bool,
|
||||||
pub search_regex: bool,
|
pub search_regex: bool,
|
||||||
pub ignore_case: bool,
|
pub ignore_case: bool,
|
||||||
pub plugins: BTreeMap<String, VecDeque<String>>,
|
#[serde(default)]
|
||||||
|
pub plugins: BTreeMap<String, Vec<String>>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub keybinds: KeyConfig,
|
||||||
|
#[serde(default)]
|
||||||
|
pub window: WindowConfig,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Config {
|
impl Default for Config {
|
||||||
@ -18,6 +148,8 @@ impl Default for Config {
|
|||||||
search_regex: false,
|
search_regex: false,
|
||||||
ignore_case: true,
|
ignore_case: true,
|
||||||
plugins: Default::default(),
|
plugins: Default::default(),
|
||||||
|
keybinds: Default::default(),
|
||||||
|
window: Default::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
13
rmenu/src/exec.rs
Normal file
13
rmenu/src/exec.rs
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
//! Execution Implementation for Entry Actions
|
||||||
|
use std::os::unix::process::CommandExt;
|
||||||
|
use std::process::Command;
|
||||||
|
|
||||||
|
use rmenu_plugin::Action;
|
||||||
|
|
||||||
|
pub fn execute(action: &Action) {
|
||||||
|
let args = match shell_words::split(&action.exec) {
|
||||||
|
Ok(args) => args,
|
||||||
|
Err(err) => panic!("{:?} invalid command {err}", action.exec),
|
||||||
|
};
|
||||||
|
Command::new(&args[0]).args(&args[1..]).exec();
|
||||||
|
}
|
108
rmenu/src/gui.rs
108
rmenu/src/gui.rs
@ -1,15 +1,36 @@
|
|||||||
|
//! RMENU GUI Implementation using Dioxus
|
||||||
#![allow(non_snake_case)]
|
#![allow(non_snake_case)]
|
||||||
use dioxus::prelude::*;
|
use dioxus::prelude::*;
|
||||||
use keyboard_types::{Code, Modifiers};
|
use keyboard_types::{Code, Modifiers};
|
||||||
use rmenu_plugin::Entry;
|
use rmenu_plugin::Entry;
|
||||||
|
|
||||||
use crate::config::Config;
|
use crate::config::{Config, Keybind};
|
||||||
|
use crate::exec::execute;
|
||||||
use crate::search::new_searchfn;
|
use crate::search::new_searchfn;
|
||||||
use crate::state::PosTracker;
|
use crate::state::PosTracker;
|
||||||
use crate::App;
|
use crate::App;
|
||||||
|
|
||||||
|
/// spawn and run the app on the configured platform
|
||||||
pub fn run(app: App) {
|
pub fn run(app: App) {
|
||||||
dioxus_desktop::launch_with_props(App, app, dioxus_desktop::Config::default());
|
// customize window
|
||||||
|
let theme = match app.config.window.dark_mode {
|
||||||
|
Some(dark) => match dark {
|
||||||
|
true => Some(dioxus_desktop::tao::window::Theme::Dark),
|
||||||
|
false => Some(dioxus_desktop::tao::window::Theme::Light),
|
||||||
|
},
|
||||||
|
None => None,
|
||||||
|
};
|
||||||
|
let builder = dioxus_desktop::WindowBuilder::new()
|
||||||
|
.with_title(app.config.window.title.clone())
|
||||||
|
.with_inner_size(app.config.window.size)
|
||||||
|
.with_position(app.config.window.position)
|
||||||
|
.with_focused(app.config.window.focus)
|
||||||
|
.with_decorations(app.config.window.decorate)
|
||||||
|
.with_transparent(app.config.window.transparent)
|
||||||
|
.with_always_on_top(app.config.window.always_top)
|
||||||
|
.with_theme(theme);
|
||||||
|
let config = dioxus_desktop::Config::new().with_window(builder);
|
||||||
|
dioxus_desktop::launch_with_props(App, app, config);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(PartialEq, Props)]
|
#[derive(PartialEq, Props)]
|
||||||
@ -21,6 +42,7 @@ struct GEntry<'a> {
|
|||||||
subpos: usize,
|
subpos: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// render a single result entry w/ the given information
|
||||||
fn TableEntry<'a>(cx: Scope<'a, GEntry<'a>>) -> Element<'a> {
|
fn TableEntry<'a>(cx: Scope<'a, GEntry<'a>>) -> Element<'a> {
|
||||||
// build css classes for result and actions (if nessesary)
|
// build css classes for result and actions (if nessesary)
|
||||||
let main_select = cx.props.index == cx.props.pos;
|
let main_select = cx.props.index == cx.props.pos;
|
||||||
@ -29,6 +51,10 @@ fn TableEntry<'a>(cx: Scope<'a, GEntry<'a>>) -> Element<'a> {
|
|||||||
true => "active",
|
true => "active",
|
||||||
false => "",
|
false => "",
|
||||||
};
|
};
|
||||||
|
let multi_classes = match cx.props.entry.actions.len() > 1 {
|
||||||
|
true => "submenu",
|
||||||
|
false => "",
|
||||||
|
};
|
||||||
let result_classes = match main_select && !action_select {
|
let result_classes = match main_select && !action_select {
|
||||||
true => "selected",
|
true => "selected",
|
||||||
false => "",
|
false => "",
|
||||||
@ -65,7 +91,19 @@ fn TableEntry<'a>(cx: Scope<'a, GEntry<'a>>) -> Element<'a> {
|
|||||||
cx.render(rsx! {
|
cx.render(rsx! {
|
||||||
div {
|
div {
|
||||||
id: "result-{cx.props.index}",
|
id: "result-{cx.props.index}",
|
||||||
class: "result {result_classes}",
|
class: "result {result_classes} {multi_classes}",
|
||||||
|
ondblclick: |_| {
|
||||||
|
let action = match cx.props.entry.actions.get(0) {
|
||||||
|
Some(action) => action,
|
||||||
|
None => {
|
||||||
|
let name = &cx.props.entry.name;
|
||||||
|
log::warn!("no action to execute on {:?}", name);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
log::info!("executing: {:?}", action.exec);
|
||||||
|
execute(action);
|
||||||
|
},
|
||||||
if cx.props.config.use_icons {
|
if cx.props.config.use_icons {
|
||||||
cx.render(rsx! {
|
cx.render(rsx! {
|
||||||
div {
|
div {
|
||||||
@ -83,7 +121,7 @@ fn TableEntry<'a>(cx: Scope<'a, GEntry<'a>>) -> Element<'a> {
|
|||||||
div {
|
div {
|
||||||
class: "comment",
|
class: "comment",
|
||||||
if let Some(comment) = cx.props.entry.comment.as_ref() {
|
if let Some(comment) = cx.props.entry.comment.as_ref() {
|
||||||
format!("- {comment}")
|
comment.to_string()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -95,37 +133,54 @@ fn TableEntry<'a>(cx: Scope<'a, GEntry<'a>>) -> Element<'a> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn focus<T>(cx: Scope<T>) {
|
||||||
|
let eval = dioxus_desktop::use_eval(cx);
|
||||||
|
let js = "document.getElementById(`search`).focus()";
|
||||||
|
eval(js.to_owned());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// check if the current inputs match any of the given keybindings
|
||||||
|
#[inline]
|
||||||
|
fn matches(bind: &Vec<Keybind>, mods: &Modifiers, key: &Code) -> bool {
|
||||||
|
bind.iter().any(|b| mods.contains(b.mods) && &b.key == key)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// main application function/loop
|
||||||
fn App(cx: Scope<App>) -> Element {
|
fn App(cx: Scope<App>) -> Element {
|
||||||
|
let quit = use_state(cx, || false);
|
||||||
let search = use_state(cx, || "".to_string());
|
let search = use_state(cx, || "".to_string());
|
||||||
|
|
||||||
// retrieve build results tracker
|
// handle exit check
|
||||||
|
if *quit.get() {
|
||||||
|
std::process::exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// retrieve results build and build position-tracker
|
||||||
let results = &cx.props.entries;
|
let results = &cx.props.entries;
|
||||||
let tracker = PosTracker::new(cx, results);
|
let tracker = PosTracker::new(cx, results);
|
||||||
let (pos, subpos) = tracker.position();
|
let (pos, subpos) = tracker.position();
|
||||||
println!("pos: {pos}, {subpos}");
|
log::debug!("pos: {pos}, {subpos}");
|
||||||
|
|
||||||
// keyboard events
|
// keyboard events
|
||||||
let eval = dioxus_desktop::use_eval(cx);
|
let keybinds = &cx.props.config.keybinds;
|
||||||
let change_evt = move |evt: KeyboardEvent| {
|
let keyboard_evt = move |evt: KeyboardEvent| {
|
||||||
match evt.code() {
|
let key = &evt.code();
|
||||||
// modify position
|
let mods = &evt.modifiers();
|
||||||
Code::ArrowUp => tracker.shift_up(),
|
log::debug!("key: {key:?} mods: {mods:?}");
|
||||||
Code::ArrowDown => tracker.shift_down(),
|
if matches(&keybinds.exit, mods, key) {
|
||||||
Code::Tab => match evt.modifiers().contains(Modifiers::SHIFT) {
|
quit.set(true);
|
||||||
true => {
|
} else if matches(&keybinds.move_up, mods, key) {
|
||||||
println!("close menu");
|
tracker.shift_up();
|
||||||
tracker.close_menu()
|
} else if matches(&keybinds.move_down, mods, key) {
|
||||||
}
|
tracker.shift_down();
|
||||||
false => {
|
} else if matches(&keybinds.open_menu, mods, key) {
|
||||||
println!("open menu!");
|
tracker.open_menu();
|
||||||
tracker.open_menu()
|
} else if matches(&keybinds.close_menu, mods, key) {
|
||||||
}
|
tracker.close_menu();
|
||||||
},
|
|
||||||
_ => println!("key: {:?}", evt.key()),
|
|
||||||
}
|
}
|
||||||
// always set focus back on input
|
// always set focus back on input
|
||||||
let js = "document.getElementById(`search`).focus()";
|
focus(cx);
|
||||||
eval(js.to_owned());
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// pre-render results into elements
|
// pre-render results into elements
|
||||||
@ -150,7 +205,8 @@ fn App(cx: Scope<App>) -> Element {
|
|||||||
cx.render(rsx! {
|
cx.render(rsx! {
|
||||||
style { "{cx.props.css}" }
|
style { "{cx.props.css}" }
|
||||||
div {
|
div {
|
||||||
onkeydown: change_evt,
|
onkeydown: keyboard_evt,
|
||||||
|
onclick: |_| focus(cx),
|
||||||
input {
|
input {
|
||||||
id: "search",
|
id: "search",
|
||||||
value: "{search}",
|
value: "{search}",
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
use std::collections::VecDeque;
|
||||||
use std::fmt::Display;
|
use std::fmt::Display;
|
||||||
use std::fs::{read_to_string, File};
|
use std::fs::{read_to_string, File};
|
||||||
use std::io::{self, prelude::*, BufReader};
|
use std::io::{self, prelude::*, BufReader};
|
||||||
@ -5,6 +6,7 @@ use std::process::{Command, ExitStatus, Stdio};
|
|||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
mod config;
|
mod config;
|
||||||
|
mod exec;
|
||||||
mod gui;
|
mod gui;
|
||||||
mod search;
|
mod search;
|
||||||
mod state;
|
mod state;
|
||||||
@ -148,7 +150,10 @@ impl Args {
|
|||||||
return Err(RMenuError::NoSuchPlugin(plugin.to_owned()));
|
return Err(RMenuError::NoSuchPlugin(plugin.to_owned()));
|
||||||
};
|
};
|
||||||
// build command
|
// build command
|
||||||
let mut cmdargs = args.clone();
|
let mut cmdargs: VecDeque<String> = args
|
||||||
|
.iter()
|
||||||
|
.map(|arg| shellexpand::tilde(arg).to_string())
|
||||||
|
.collect();
|
||||||
let Some(main) = cmdargs.pop_front() else {
|
let Some(main) = cmdargs.pop_front() else {
|
||||||
return Err(RMenuError::InvalidPlugin(plugin.to_owned()));
|
return Err(RMenuError::InvalidPlugin(plugin.to_owned()));
|
||||||
};
|
};
|
||||||
@ -188,6 +193,7 @@ impl Args {
|
|||||||
config.css.extend(args.css.clone());
|
config.css.extend(args.css.clone());
|
||||||
let mut css = vec![];
|
let mut css = vec![];
|
||||||
for path in config.css.iter() {
|
for path in config.css.iter() {
|
||||||
|
let path = shellexpand::tilde(path).to_string();
|
||||||
let src = read_to_string(path)?;
|
let src = read_to_string(path)?;
|
||||||
css.push(src);
|
css.push(src);
|
||||||
}
|
}
|
||||||
@ -211,8 +217,7 @@ impl Args {
|
|||||||
|
|
||||||
//TODO: config
|
//TODO: config
|
||||||
// - default and cli accessable modules (instead of piped in)
|
// - default and cli accessable modules (instead of piped in)
|
||||||
// - allow/disable icons (also available via CLI)
|
// - should resolve arguments/paths with home expansion
|
||||||
// - custom keybindings (some available via CLI?)
|
|
||||||
|
|
||||||
//TODO: add exit key (Esc by default?) - part of keybindings
|
//TODO: add exit key (Esc by default?) - part of keybindings
|
||||||
|
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
//! RMENU Entry Search Function Implementaton
|
||||||
use regex::RegexBuilder;
|
use regex::RegexBuilder;
|
||||||
use rmenu_plugin::Entry;
|
use rmenu_plugin::Entry;
|
||||||
|
|
||||||
@ -29,7 +30,7 @@ macro_rules! search {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Generate a new dynamic Search Function based on
|
/// Generate a new dynamic Search Function based on
|
||||||
/// Configurtaion Settigns and Search-String
|
/// Configurtaion Settings and Search-String
|
||||||
pub fn new_searchfn(cfg: &Config, search: &str) -> Box<dyn Fn(&Entry) -> bool> {
|
pub fn new_searchfn(cfg: &Config, search: &str) -> Box<dyn Fn(&Entry) -> bool> {
|
||||||
if cfg.search_regex {
|
if cfg.search_regex {
|
||||||
let regex = RegexBuilder::new(search)
|
let regex = RegexBuilder::new(search)
|
||||||
|
@ -1,15 +1,26 @@
|
|||||||
/// Application State Trackers and Utilities
|
//! GUI Application State Trackers and Utilities
|
||||||
use dioxus::prelude::{use_state, Scope, UseState};
|
use dioxus::prelude::{use_state, Scope, UseState};
|
||||||
use rmenu_plugin::Entry;
|
use rmenu_plugin::Entry;
|
||||||
|
|
||||||
use crate::App;
|
use crate::App;
|
||||||
|
|
||||||
|
#[derive(PartialEq)]
|
||||||
pub struct PosTracker<'a> {
|
pub struct PosTracker<'a> {
|
||||||
pos: &'a UseState<usize>,
|
pos: &'a UseState<usize>,
|
||||||
subpos: &'a UseState<usize>,
|
subpos: &'a UseState<usize>,
|
||||||
results: &'a Vec<Entry>,
|
results: &'a Vec<Entry>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<'a> Clone for PosTracker<'a> {
|
||||||
|
fn clone(&self) -> Self {
|
||||||
|
Self {
|
||||||
|
pos: self.pos,
|
||||||
|
subpos: self.subpos,
|
||||||
|
results: self.results,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl<'a> PosTracker<'a> {
|
impl<'a> PosTracker<'a> {
|
||||||
pub fn new(cx: Scope<'a, App>, results: &'a Vec<Entry>) -> Self {
|
pub fn new(cx: Scope<'a, App>, results: &'a Vec<Entry>) -> Self {
|
||||||
let pos = use_state(cx, || 0);
|
let pos = use_state(cx, || 0);
|
||||||
@ -60,7 +71,6 @@ impl<'a> PosTracker<'a> {
|
|||||||
let index = *self.pos.get();
|
let index = *self.pos.get();
|
||||||
let result = &self.results[index];
|
let result = &self.results[index];
|
||||||
let subpos = *self.subpos.get();
|
let subpos = *self.subpos.get();
|
||||||
println!("modify subpos? {} {}", subpos, result.actions.len());
|
|
||||||
if subpos > 0 && subpos < result.actions.len() - 1 {
|
if subpos > 0 && subpos < result.actions.len() - 1 {
|
||||||
self.subpos.modify(|v| v + 1);
|
self.subpos.modify(|v| v + 1);
|
||||||
return;
|
return;
|
||||||
|
Loading…
Reference in New Issue
Block a user