Skip to content

Commit

Permalink
Check if last pressed key is part of hotkey combination (#120)
Browse files Browse the repository at this point in the history
* First attempt at fixing #119

* Reset last_pressed on key up + refactor

* Refactor key handling logic

* Handle conflict with #127

* Temporary debug message for last key match

* Another temporary debug message

* Remove temporary logs + refactor

* Add feature for using event key

* Update README with feature flag description

* Revert "Update README with feature flag description"

This reverts commit f721c9b.

* Revert "Add feature for using event key"

This reverts commit f1e5410.

---------

Co-authored-by: Matthew Kim <[email protected]>
  • Loading branch information
maxbergmark and friendlymatthew authored Sep 3, 2024
1 parent 9c66ebb commit daa7845
Show file tree
Hide file tree
Showing 3 changed files with 94 additions and 51 deletions.
54 changes: 36 additions & 18 deletions leptos_hotkeys/src/context.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
use leptos::html::ElementDescriptor;
use leptos::*;
use std::collections::HashSet;
use std::collections::{BTreeMap, HashSet};
#[cfg(not(feature = "ssr"))]
use wasm_bindgen::JsCast;

#[derive(Clone, Copy)]
pub struct HotkeysContext {
#[cfg(not(feature = "ssr"))]
pub(crate) pressed_keys: RwSignal<std::collections::HashMap<String, web_sys::KeyboardEvent>>,
pub(crate) keys_pressed: RwSignal<KeyPresses>,

#[cfg(not(feature = "ssr"))]
pub active_ref_target: RwSignal<Option<web_sys::EventTarget>>,
Expand All @@ -20,6 +20,12 @@ pub struct HotkeysContext {
pub disable_scope: Callback<String>,
pub toggle_scope: Callback<String>,
}
#[derive(Debug, Default, Clone)]
#[cfg_attr(feature = "ssr", allow(dead_code))]
pub struct KeyPresses {
pub key_map: BTreeMap<String, web_sys::KeyboardEvent>,
pub last_key: Option<String>,
}

pub fn provide_hotkeys_context<T>(
#[cfg_attr(feature = "ssr", allow(unused_variables))] node_ref: NodeRef<T>,
Expand All @@ -38,8 +44,7 @@ where
});

#[cfg(not(feature = "ssr"))]
let pressed_keys: RwSignal<std::collections::HashMap<String, web_sys::KeyboardEvent>> =
RwSignal::new(std::collections::HashMap::new());
let keys_pressed: RwSignal<KeyPresses> = RwSignal::new(KeyPresses::default());

let active_scopes: RwSignal<HashSet<String>> = RwSignal::new(initially_active_scopes);

Expand Down Expand Up @@ -81,8 +86,15 @@ where

#[cfg(all(feature = "debug", not(feature = "ssr")))]
create_effect(move |_| {
let pressed_keys_list = move || pressed_keys.get().keys().cloned().collect::<Vec<String>>();
logging::log!("keys pressed: {:?}", pressed_keys_list());
let keys_pressed_list = move || {
keys_pressed
.get()
.key_map
.keys()
.cloned()
.collect::<Vec<String>>()
};
logging::log!("keys pressed: {:?}", keys_pressed_list());
});

#[cfg(not(feature = "ssr"))]
Expand All @@ -91,25 +103,23 @@ where
if cfg!(feature = "debug") {
logging::log!("Window lost focus");
}
pressed_keys.set_untracked(std::collections::HashMap::new());
keys_pressed.set_untracked(KeyPresses::default());
}) as Box<dyn Fn()>);

let keydown_listener =
wasm_bindgen::closure::Closure::wrap(Box::new(move |event: web_sys::KeyboardEvent| {
pressed_keys.update(|keys| {
match &event.key().eq_ignore_ascii_case(" ") {
true => keys.insert("spacebar".to_string(), event),
false => keys.insert(event.key().to_lowercase(), event),
};
keys_pressed.update(|keys| {
let key = clean_key(&event);
keys.key_map.insert(key.clone(), event);
keys.last_key = Some(key);
});
}) as Box<dyn Fn(_)>);
let keyup_listener =
wasm_bindgen::closure::Closure::wrap(Box::new(move |event: web_sys::KeyboardEvent| {
pressed_keys.update(|keys| {
match &event.key().eq_ignore_ascii_case(" ") {
true => keys.remove(&"spacebar".to_string()),
false => keys.remove(&event.key().to_lowercase()),
};
keys_pressed.update(|keys| {
let key = clean_key(&event);
keys.key_map.remove(&key);
keys.last_key = None;
});
}) as Box<dyn Fn(_)>);

Expand Down Expand Up @@ -156,7 +166,7 @@ where

let hotkeys_context = HotkeysContext {
#[cfg(not(feature = "ssr"))]
pressed_keys,
keys_pressed,

#[cfg(not(feature = "ssr"))]
active_ref_target,
Expand All @@ -177,3 +187,11 @@ where
pub fn use_hotkeys_context() -> HotkeysContext {
use_context::<HotkeysContext>().expect("expected hotkeys context")
}

#[cfg(not(feature = "ssr"))]
fn clean_key(event: &web_sys::KeyboardEvent) -> String {
match event.key().as_str() {
" " => "spacebar".to_string(),
_ => event.key().to_lowercase(),
}
}
18 changes: 16 additions & 2 deletions leptos_hotkeys/src/hotkey.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use crate::types::Keys;
use crate::KeyboardModifiers;
use crate::{context::KeyPresses, types::Keys};
use core::str::FromStr;
use std::collections::HashSet;
use std::fmt::{Display, Formatter};

#[derive(Debug, PartialEq, Hash, Eq)]
Expand Down Expand Up @@ -29,6 +30,10 @@ impl Hotkey {
pub fn new(key_combination: &str) -> Self {
key_combination.parse().unwrap()
}

fn includes_key(&self, key: &String) -> bool {
self.keys.iter().any(|k| k == key)
}
}

impl FromStr for Hotkey {
Expand Down Expand Up @@ -77,10 +82,19 @@ impl FromStr for Hotkey {
}
}

#[cfg_attr(feature = "ssr", allow(dead_code))]
pub(crate) fn is_last_key_match(parsed_keys: &HashSet<Hotkey>, pressed_keys: &KeyPresses) -> bool {
pressed_keys.last_key.as_ref().is_some_and(|last_key| {
parsed_keys
.iter()
.any(|hotkey| hotkey.includes_key(last_key))
})
}

#[cfg_attr(feature = "ssr", allow(dead_code))]
pub(crate) fn is_hotkey_match(
hotkey: &Hotkey,
pressed_keyset: &mut std::collections::HashMap<String, web_sys::KeyboardEvent>,
pressed_keyset: &mut std::collections::BTreeMap<String, web_sys::KeyboardEvent>,
) -> bool {
let mut modifiers_match = true;

Expand Down
73 changes: 42 additions & 31 deletions leptos_hotkeys/src/use_hotkeys.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,34 +7,39 @@ pub fn use_hotkeys_scoped(
) {
#[cfg(not(feature = "ssr"))]
{
use crate::hotkey::is_hotkey_match;
use crate::hotkey::{is_hotkey_match, is_last_key_match};
use crate::{use_hotkeys_context, Hotkey};
use std::collections::HashSet;

let parsed_keys: HashSet<Hotkey> = key_combination.split(',').map(Hotkey::new).collect();

let hotkeys_context = use_hotkeys_context();
let pressed_keys = hotkeys_context.pressed_keys;

create_effect(move |_| {
let active_scopes = hotkeys_context.active_scopes.get();
let within_scope = scopes.iter().any(|scope| active_scopes.contains(scope));

if within_scope {
let mut pressed_keyset = pressed_keys.get();
if let Some(matching_hotkey) = parsed_keys
.iter()
.find(|hotkey| is_hotkey_match(hotkey, &mut pressed_keyset))
{
if cfg!(feature = "debug") {
let message = format!("%cfiring hotkey: {}", &matching_hotkey);
web_sys::console::log_2(
&wasm_bindgen::JsValue::from_str(&message),
&wasm_bindgen::JsValue::from_str("color: #39FF14;"),
);
}
Callable::call(&on_triggered, ());
if !within_scope {
return;
}

let mut keys_pressed = hotkeys_context.keys_pressed.get();
if !is_last_key_match(&parsed_keys, &keys_pressed) {
return;
}

if let Some(matching_hotkey) = parsed_keys
.iter()
.find(|hotkey| is_hotkey_match(hotkey, &mut keys_pressed.key_map))
{
if cfg!(feature = "debug") {
let message = format!("%cfiring hotkey: {}", &matching_hotkey);
web_sys::console::log_2(
&wasm_bindgen::JsValue::from_str(&message),
&wasm_bindgen::JsValue::from_str("color: #39FF14;"),
);
}
Callable::call(&on_triggered, ());
}
});
}
Expand All @@ -50,7 +55,7 @@ pub fn use_hotkeys_ref<T>(
{
#[cfg(not(feature = "ssr"))]
create_effect(move |_| {
use crate::hotkey::is_hotkey_match;
use crate::hotkey::{is_hotkey_match, is_last_key_match};
use crate::{use_hotkeys_context, Hotkey};
use leptos::ev::DOMEventResponder;
use std::collections::HashSet;
Expand All @@ -61,23 +66,29 @@ pub fn use_hotkeys_ref<T>(
let keydown_closure = move |_event: web_sys::KeyboardEvent| {
let hotkeys_context = use_hotkeys_context();
let active_scopes = hotkeys_context.active_scopes.get();
let mut pressed_keys = hotkeys_context.pressed_keys.get();
let mut pressed_keys = hotkeys_context.keys_pressed.get();
let within_scope = scopes.iter().any(|scope| active_scopes.contains(scope));

if within_scope {
if let Some(matching_hotkey) = parsed_keys
.iter()
.find(|hotkey| is_hotkey_match(hotkey, &mut pressed_keys))
{
if cfg!(feature = "debug") {
let message = format!("%cfiring hotkey: {}", &matching_hotkey);
web_sys::console::log_2(
&wasm_bindgen::JsValue::from_str(&message),
&wasm_bindgen::JsValue::from_str("color: #39FF14;"),
);
}
Callable::call(&on_triggered, ());
if !within_scope {
return;
}

if !is_last_key_match(&parsed_keys, &pressed_keys) {
return;
}

if let Some(matching_hotkey) = parsed_keys
.iter()
.find(|hotkey| is_hotkey_match(hotkey, &mut pressed_keys.key_map))
{
if cfg!(feature = "debug") {
let message = format!("%cfiring hotkey: {}", &matching_hotkey);
web_sys::console::log_2(
&wasm_bindgen::JsValue::from_str(&message),
&wasm_bindgen::JsValue::from_str("color: #39FF14;"),
);
}
Callable::call(&on_triggered, ());
}
};

Expand Down

0 comments on commit daa7845

Please sign in to comment.