From bb79bbd0e40e88aad25423b3c99dae4a70a0fb6d Mon Sep 17 00:00:00 2001 From: Simon Desloges Date: Sun, 11 Jul 2021 23:46:00 -0400 Subject: [PATCH] Add modifier keys to every keyboard and mouse input (#247) * Add modifier keys to every keyboard and mouse input * Fix bad naming convention in Typescript * Added some tests related to modifier keys --- client/web/src/components/panels/Document.vue | 20 ++- client/web/wasm/src/document.rs | 30 +++-- core/editor/src/input/input_preprocessor.rs | 127 ++++++++++++++++-- core/editor/src/input/keyboard.rs | 4 + core/editor/src/input/mod.rs | 2 +- core/editor/src/misc/test_utils.rs | 8 +- 6 files changed, 159 insertions(+), 32 deletions(-) diff --git a/client/web/src/components/panels/Document.vue b/client/web/src/components/panels/Document.vue index 2c1d3cd8..c1182739 100644 --- a/client/web/src/components/panels/Document.vue +++ b/client/web/src/components/panels/Document.vue @@ -228,32 +228,42 @@ function redirectKeyboardEventToBackend(e: KeyboardEvent): boolean { return true; } +function makeModifiersBitfield(control: boolean, shift: boolean, alt: boolean): number { + // eslint-disable-next-line no-bitwise + return Number(control) | (Number(shift) << 1) | (Number(alt) << 2); +} + export default defineComponent({ methods: { async canvasMouseDown(e: MouseEvent) { const { on_mouse_down } = await wasm; - on_mouse_down(e.offsetX, e.offsetY, e.buttons); + const modifiers = makeModifiersBitfield(e.ctrlKey, e.shiftKey, e.altKey); + on_mouse_down(e.offsetX, e.offsetY, e.buttons, modifiers); }, async canvasMouseUp(e: MouseEvent) { const { on_mouse_up } = await wasm; - on_mouse_up(e.offsetX, e.offsetY, e.buttons); + const modifiers = makeModifiersBitfield(e.ctrlKey, e.shiftKey, e.altKey); + on_mouse_up(e.offsetX, e.offsetY, e.buttons, modifiers); }, async canvasMouseMove(e: MouseEvent) { const { on_mouse_move } = await wasm; - on_mouse_move(e.offsetX, e.offsetY); + const modifiers = makeModifiersBitfield(e.ctrlKey, e.shiftKey, e.altKey); + on_mouse_move(e.offsetX, e.offsetY, modifiers); }, async keyDown(e: KeyboardEvent) { if (redirectKeyboardEventToBackend(e)) { e.preventDefault(); const { on_key_down } = await wasm; - on_key_down(e.key); + const modifiers = makeModifiersBitfield(e.ctrlKey, e.shiftKey, e.altKey); + on_key_down(e.key, modifiers); } }, async keyUp(e: KeyboardEvent) { if (redirectKeyboardEventToBackend(e)) { e.preventDefault(); const { on_key_up } = await wasm; - on_key_up(e.key); + const modifiers = makeModifiersBitfield(e.ctrlKey, e.shiftKey, e.altKey); + on_key_up(e.key, modifiers); } }, async selectTool(toolName: string) { diff --git a/client/web/wasm/src/document.rs b/client/web/wasm/src/document.rs index 03927717..daa1eb64 100644 --- a/client/web/wasm/src/document.rs +++ b/client/web/wasm/src/document.rs @@ -1,6 +1,7 @@ use crate::shims::Error; use crate::wrappers::{translate_key, translate_tool, Color}; use crate::EDITOR_STATE; +use editor_core::input::input_preprocessor::ModifierKeys; use editor_core::message_prelude::*; use editor_core::{ input::mouse::{MouseState, ViewportPosition}, @@ -39,43 +40,48 @@ pub fn new_document() -> Result<(), JsValue> { // TODO: When a mouse button is down that started in the viewport, this should trigger even when the mouse is outside the viewport (or even the browser window if the browser supports it) /// Mouse movement within the screenspace bounds of the viewport #[wasm_bindgen] -pub fn on_mouse_move(x: u32, y: u32) -> Result<(), JsValue> { +pub fn on_mouse_move(x: u32, y: u32, modifiers: u8) -> Result<(), JsValue> { + let mods = ModifierKeys::from_bits(modifiers).expect("invalid modifier keys"); // TODO: Convert these screenspace viewport coordinates to canvas coordinates based on the current zoom and pan - let ev = InputPreprocessorMessage::MouseMove(ViewportPosition { x, y }); + let ev = InputPreprocessorMessage::MouseMove(ViewportPosition { x, y }, mods); EDITOR_STATE.with(|editor| editor.borrow_mut().handle_message(ev)).map_err(convert_error) } /// A mouse button depressed within screenspace the bounds of the viewport #[wasm_bindgen] -pub fn on_mouse_down(x: u32, y: u32, mouse_keys: u8) -> Result<(), JsValue> { +pub fn on_mouse_down(x: u32, y: u32, mouse_keys: u8, modifiers: u8) -> Result<(), JsValue> { let pos = ViewportPosition { x, y }; - let ev = InputPreprocessorMessage::MouseDown(MouseState::from_u8_pos(mouse_keys, pos)); + let mods = ModifierKeys::from_bits(modifiers).expect("invalid modifier keys"); + let ev = InputPreprocessorMessage::MouseDown(MouseState::from_u8_pos(mouse_keys, pos), mods); EDITOR_STATE.with(|editor| editor.borrow_mut().handle_message(ev)).map_err(convert_error) } /// A mouse button released #[wasm_bindgen] -pub fn on_mouse_up(x: u32, y: u32, mouse_keys: u8) -> Result<(), JsValue> { +pub fn on_mouse_up(x: u32, y: u32, mouse_keys: u8, modifiers: u8) -> Result<(), JsValue> { let pos = ViewportPosition { x, y }; - let ev = InputPreprocessorMessage::MouseUp(MouseState::from_u8_pos(mouse_keys, pos)); + let mods = ModifierKeys::from_bits(modifiers).expect("invalid modifier keys"); + let ev = InputPreprocessorMessage::MouseUp(MouseState::from_u8_pos(mouse_keys, pos), mods); EDITOR_STATE.with(|editor| editor.borrow_mut().handle_message(ev)).map_err(convert_error) } /// A keyboard button depressed within screenspace the bounds of the viewport #[wasm_bindgen] -pub fn on_key_down(name: String) -> Result<(), JsValue> { +pub fn on_key_down(name: String, modifiers: u8) -> Result<(), JsValue> { let key = translate_key(&name); - log::trace!("key down {:?}, name: {}", key, name); - let ev = InputPreprocessorMessage::KeyDown(key); + let mods = ModifierKeys::from_bits(modifiers).expect("invalid modifier keys"); + log::trace!("key down {:?}, name: {}, modifiers: {:?}", key, name, mods); + let ev = InputPreprocessorMessage::KeyDown(key, mods); EDITOR_STATE.with(|editor| editor.borrow_mut().handle_message(ev)).map_err(convert_error) } /// A keyboard button released #[wasm_bindgen] -pub fn on_key_up(name: String) -> Result<(), JsValue> { +pub fn on_key_up(name: String, modifiers: u8) -> Result<(), JsValue> { let key = translate_key(&name); - log::trace!("key up {:?}, name: {}", key, name); - let ev = InputPreprocessorMessage::KeyUp(key); + let mods = ModifierKeys::from_bits(modifiers).expect("invalid modifier keys"); + log::trace!("key up {:?}, name: {}, modifiers: {:?}", key, name, mods); + let ev = InputPreprocessorMessage::KeyUp(key, mods); EDITOR_STATE.with(|editor| editor.borrow_mut().handle_message(ev)).map_err(convert_error) } diff --git a/core/editor/src/input/input_preprocessor.rs b/core/editor/src/input/input_preprocessor.rs index c5e34565..5dfac2f3 100644 --- a/core/editor/src/input/input_preprocessor.rs +++ b/core/editor/src/input/input_preprocessor.rs @@ -1,6 +1,9 @@ +use std::usize; + use super::keyboard::{Key, KeyStates}; use super::mouse::{MouseKeys, MouseState, ViewportPosition}; use crate::message_prelude::*; +use bitflags::bitflags; #[doc(inline)] pub use document_core::DocumentResponse; @@ -8,11 +11,21 @@ pub use document_core::DocumentResponse; #[impl_message(Message, InputPreprocessor)] #[derive(PartialEq, Clone, Debug)] pub enum InputPreprocessorMessage { - MouseDown(MouseState), - MouseUp(MouseState), - MouseMove(ViewportPosition), - KeyUp(Key), - KeyDown(Key), + MouseDown(MouseState, ModifierKeys), + MouseUp(MouseState, ModifierKeys), + MouseMove(ViewportPosition, ModifierKeys), + KeyUp(Key, ModifierKeys), + KeyDown(Key, ModifierKeys), +} + +bitflags! { + #[derive(Default)] + #[repr(transparent)] + pub struct ModifierKeys: u8 { + const CONTROL = 0b0000_0001; + const SHIFT = 0b0000_0010; + const ALT = 0b0000_0100; + } } #[derive(Debug, Default)] @@ -29,17 +42,26 @@ enum KeyPosition { impl MessageHandler for InputPreprocessor { fn process_action(&mut self, message: InputPreprocessorMessage, _data: (), responses: &mut VecDeque) { let response = match message { - InputPreprocessorMessage::MouseMove(pos) => { + InputPreprocessorMessage::MouseMove(pos, modifier_keys) => { + self.handle_modifier_keys(modifier_keys, responses); self.mouse.position = pos; InputMapperMessage::PointerMove.into() } - InputPreprocessorMessage::MouseDown(state) => self.translate_mouse_event(state, KeyPosition::Pressed), - InputPreprocessorMessage::MouseUp(state) => self.translate_mouse_event(state, KeyPosition::Released), - InputPreprocessorMessage::KeyDown(key) => { + InputPreprocessorMessage::MouseDown(state, modifier_keys) => { + self.handle_modifier_keys(modifier_keys, responses); + self.translate_mouse_event(state, KeyPosition::Pressed) + } + InputPreprocessorMessage::MouseUp(state, modifier_keys) => { + self.handle_modifier_keys(modifier_keys, responses); + self.translate_mouse_event(state, KeyPosition::Released) + } + InputPreprocessorMessage::KeyDown(key, modifier_keys) => { + self.handle_modifier_keys(modifier_keys, responses); self.keyboard.set(key as usize); InputMapperMessage::KeyDown(key).into() } - InputPreprocessorMessage::KeyUp(key) => { + InputPreprocessorMessage::KeyUp(key, modifier_keys) => { + self.handle_modifier_keys(modifier_keys, responses); self.keyboard.unset(key as usize); InputMapperMessage::KeyUp(key).into() } @@ -71,4 +93,89 @@ impl InputPreprocessor { KeyPosition::Released => InputMapperMessage::KeyUp(key).into(), } } + + fn handle_modifier_keys(&mut self, modifier_keys: ModifierKeys, responses: &mut VecDeque) { + self.handle_modifier_key(Key::KeyControl, modifier_keys.contains(ModifierKeys::CONTROL), responses); + self.handle_modifier_key(Key::KeyShift, modifier_keys.contains(ModifierKeys::SHIFT), responses); + self.handle_modifier_key(Key::KeyAlt, modifier_keys.contains(ModifierKeys::ALT), responses); + } + + fn handle_modifier_key(&mut self, key: Key, key_is_down: bool, responses: &mut VecDeque) { + let key_was_down = self.keyboard.get(key as usize); + if key_was_down && !key_is_down { + self.keyboard.unset(key as usize); + responses.push_back(InputMapperMessage::KeyUp(key).into()); + } else if !key_was_down && key_is_down { + self.keyboard.set(key as usize); + responses.push_back(InputMapperMessage::KeyDown(key).into()); + } + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn process_action_mouse_move_handle_modifier_keys() { + let mut input_preprocessor = InputPreprocessor::default(); + let message = InputPreprocessorMessage::MouseMove(ViewportPosition { x: 4, y: 809 }, ModifierKeys::ALT); + let mut responses = VecDeque::new(); + + input_preprocessor.process_action(message, (), &mut responses); + + assert!(input_preprocessor.keyboard.get(Key::KeyAlt as usize)); + assert_eq!(responses.pop_front(), Some(InputMapperMessage::KeyDown(Key::KeyAlt).into())); + } + + #[test] + fn process_action_mouse_down_handle_modifier_keys() { + let mut input_preprocessor = InputPreprocessor::default(); + let message = InputPreprocessorMessage::MouseDown(MouseState::new(), ModifierKeys::CONTROL); + let mut responses = VecDeque::new(); + + input_preprocessor.process_action(message, (), &mut responses); + + assert!(input_preprocessor.keyboard.get(Key::KeyControl as usize)); + assert_eq!(responses.pop_front(), Some(InputMapperMessage::KeyDown(Key::KeyControl).into())); + } + + #[test] + fn process_action_mouse_up_handle_modifier_keys() { + let mut input_preprocessor = InputPreprocessor::default(); + let message = InputPreprocessorMessage::MouseUp(MouseState::new(), ModifierKeys::SHIFT); + let mut responses = VecDeque::new(); + + input_preprocessor.process_action(message, (), &mut responses); + + assert!(input_preprocessor.keyboard.get(Key::KeyShift as usize)); + assert_eq!(responses.pop_front(), Some(InputMapperMessage::KeyDown(Key::KeyShift).into())); + } + + #[test] + fn process_action_key_down_handle_modifier_keys() { + let mut input_preprocessor = InputPreprocessor::default(); + input_preprocessor.keyboard.set(Key::KeyControl as usize); + let message = InputPreprocessorMessage::KeyDown(Key::KeyA, ModifierKeys::empty()); + let mut responses = VecDeque::new(); + + input_preprocessor.process_action(message, (), &mut responses); + + assert!(!input_preprocessor.keyboard.get(Key::KeyControl as usize)); + assert_eq!(responses.pop_front(), Some(InputMapperMessage::KeyUp(Key::KeyControl).into())); + } + + #[test] + fn process_action_key_up_handle_modifier_keys() { + let mut input_preprocessor = InputPreprocessor::default(); + let message = InputPreprocessorMessage::KeyUp(Key::KeyS, ModifierKeys::CONTROL | ModifierKeys::SHIFT); + let mut responses = VecDeque::new(); + + input_preprocessor.process_action(message, (), &mut responses); + + assert!(input_preprocessor.keyboard.get(Key::KeyControl as usize)); + assert!(input_preprocessor.keyboard.get(Key::KeyShift as usize)); + assert!(responses.contains(&InputMapperMessage::KeyDown(Key::KeyControl).into())); + assert!(responses.contains(&InputMapperMessage::KeyDown(Key::KeyControl).into())); + } } diff --git a/core/editor/src/input/keyboard.rs b/core/editor/src/input/keyboard.rs index 4e8ac12f..505d483d 100644 --- a/core/editor/src/input/keyboard.rs +++ b/core/editor/src/input/keyboard.rs @@ -102,6 +102,10 @@ impl BitVector { let (offset, bit) = Self::convert_index(bitvector_index); self.0[offset] ^= bit; } + pub fn get(&mut self, bitvector_index: usize) -> bool { + let (offset, bit) = Self::convert_index(bitvector_index); + (self.0[offset] & bit) != 0 + } pub fn is_empty(&self) -> bool { let mut result = 0; for storage in self.0.iter() { diff --git a/core/editor/src/input/mod.rs b/core/editor/src/input/mod.rs index 3e68eafc..a2c50ffe 100644 --- a/core/editor/src/input/mod.rs +++ b/core/editor/src/input/mod.rs @@ -5,5 +5,5 @@ pub mod mouse; pub use { input_mapper::{InputMapper, InputMapperMessage, InputMapperMessageDiscriminant}, - input_preprocessor::{InputPreprocessor, InputPreprocessorMessage, InputPreprocessorMessageDiscriminant}, + input_preprocessor::{InputPreprocessor, InputPreprocessorMessage, InputPreprocessorMessageDiscriminant, ModifierKeys}, }; diff --git a/core/editor/src/misc/test_utils.rs b/core/editor/src/misc/test_utils.rs index fe3b03f4..d5afe8a1 100644 --- a/core/editor/src/misc/test_utils.rs +++ b/core/editor/src/misc/test_utils.rs @@ -1,7 +1,7 @@ use crate::{ input::{ mouse::{MouseKeys, MouseState, ViewportPosition}, - InputPreprocessorMessage, + InputPreprocessorMessage, ModifierKeys, }, message_prelude::{Message, ToolMessage}, tool::ToolType, @@ -51,15 +51,15 @@ impl EditorTestUtils for Editor { } fn move_mouse(&mut self, x: u32, y: u32) { - self.input(InputPreprocessorMessage::MouseMove(ViewportPosition { x, y })); + self.input(InputPreprocessorMessage::MouseMove(ViewportPosition { x, y }, ModifierKeys::default())); } fn mousedown(&mut self, state: MouseState) { - self.input(InputPreprocessorMessage::MouseDown(state)); + self.input(InputPreprocessorMessage::MouseDown(state, ModifierKeys::default())); } fn mouseup(&mut self, state: MouseState) { - self.handle_message(InputPreprocessorMessage::MouseUp(state)).unwrap() + self.handle_message(InputPreprocessorMessage::MouseUp(state, ModifierKeys::default())).unwrap() } fn lmb_mousedown(&mut self, x: u32, y: u32) {