diff --git a/editor/src/messages/input_mapper/input_mapper_message.rs b/editor/src/messages/input_mapper/input_mapper_message.rs index bfcda26c73..2f9a3d2ac2 100644 --- a/editor/src/messages/input_mapper/input_mapper_message.rs +++ b/editor/src/messages/input_mapper/input_mapper_message.rs @@ -16,6 +16,8 @@ pub enum InputMapperMessage { KeyUpNoRepeat(Key), #[child] DoubleClick(MouseButton), + #[child] + DoubleTap(Key), // Messages PointerMove, diff --git a/editor/src/messages/input_mapper/input_mapper_message_handler.rs b/editor/src/messages/input_mapper/input_mapper_message_handler.rs index d38845c6d9..50744c5820 100644 --- a/editor/src/messages/input_mapper/input_mapper_message_handler.rs +++ b/editor/src/messages/input_mapper/input_mapper_message_handler.rs @@ -40,6 +40,7 @@ impl InputMapperMessageHandler { .chain(self.mapping.key_up_no_repeat.iter()) .chain(self.mapping.key_down_no_repeat.iter()) .chain(self.mapping.double_click.iter()) + .chain(self.mapping.double_tap.iter()) .chain(std::iter::once(&self.mapping.wheel_scroll)) .chain(std::iter::once(&self.mapping.pointer_move)); let all_mapping_entries = all_key_mapping_entries.flat_map(|entry| entry.0.iter()); @@ -68,7 +69,7 @@ impl InputMapperMessageHandler { // Append the key button for the entry use InputMapperMessage as IMM; match entry.input { - IMM::KeyDown(key) | IMM::KeyUp(key) | IMM::KeyDownNoRepeat(key) | IMM::KeyUpNoRepeat(key) => keys.push(key), + IMM::KeyDown(key) | IMM::KeyUp(key) | IMM::KeyDownNoRepeat(key) | IMM::KeyUpNoRepeat(key) | IMM::DoubleTap(key) => keys.push(key), _ => (), } diff --git a/editor/src/messages/input_mapper/input_mappings.rs b/editor/src/messages/input_mapper/input_mappings.rs index 644b8d5a7b..135b92773e 100644 --- a/editor/src/messages/input_mapper/input_mappings.rs +++ b/editor/src/messages/input_mapper/input_mappings.rs @@ -337,7 +337,7 @@ pub fn input_mappings(zoom_with_scroll: bool) -> Mapping { entry!(KeyDown(Tab); action_dispatch=ToolMessage::ToggleSelectVsPath), // // DocumentMessage - entry!(KeyDown(Space); modifiers=[Control], action_dispatch=DocumentMessage::GraphViewOverlayToggle), + entry!(DoubleTap(Space); action_dispatch=DocumentMessage::GraphViewOverlayToggle), entry!(KeyDownNoRepeat(Escape); action_dispatch=DocumentMessage::Escape), entry!(KeyDown(Delete); action_dispatch=DocumentMessage::DeleteSelectedLayers), entry!(KeyDown(Backspace); action_dispatch=DocumentMessage::DeleteSelectedLayers), @@ -463,7 +463,7 @@ pub fn input_mappings(zoom_with_scroll: bool) -> Mapping { entry!(KeyDown(Space); modifiers=[Shift], action_dispatch=AnimationMessage::ToggleLivePreview), entry!(KeyDown(Home); modifiers=[Shift], action_dispatch=AnimationMessage::RestartAnimation), ]; - let (mut key_up, mut key_down, mut key_up_no_repeat, mut key_down_no_repeat, mut double_click, mut wheel_scroll, mut pointer_move, mut pointer_shake) = mappings; + let (mut key_up, mut key_down, mut key_up_no_repeat, mut key_down_no_repeat, mut double_click, mut double_tap, mut wheel_scroll, mut pointer_move, mut pointer_shake) = mappings; let sort = |list: &mut KeyMappingEntries| list.0.sort_by(|a, b| b.modifiers.count_ones().cmp(&a.modifiers.count_ones())); // Sort the sublists of `key_up`, `key_down`, `key_up_no_repeat`, and `key_down_no_repeat` @@ -476,6 +476,10 @@ pub fn input_mappings(zoom_with_scroll: bool) -> Mapping { for sublist in &mut double_click { sort(sublist) } + // Sort the sublists of `double_tap` + for sublist in &mut double_tap { + sort(sublist) + } // Sort `wheel_scroll` sort(&mut wheel_scroll); // Sort `pointer_move` @@ -489,6 +493,7 @@ pub fn input_mappings(zoom_with_scroll: bool) -> Mapping { key_up_no_repeat, key_down_no_repeat, double_click, + double_tap, wheel_scroll, pointer_move, pointer_shake, diff --git a/editor/src/messages/input_mapper/utility_types/macros.rs b/editor/src/messages/input_mapper/utility_types/macros.rs index 9dae86c9a5..c7a16e2772 100644 --- a/editor/src/messages/input_mapper/utility_types/macros.rs +++ b/editor/src/messages/input_mapper/utility_types/macros.rs @@ -119,6 +119,7 @@ macro_rules! mapping { let mut key_up_no_repeat = KeyMappingEntries::key_array(); let mut key_down_no_repeat = KeyMappingEntries::key_array(); let mut double_click = KeyMappingEntries::mouse_buttons_arrays(); + let mut double_tap = KeyMappingEntries::key_array(); let mut wheel_scroll = KeyMappingEntries::new(); let mut pointer_move = KeyMappingEntries::new(); let mut pointer_shake = KeyMappingEntries::new(); @@ -138,6 +139,7 @@ macro_rules! mapping { InputMapperMessage::KeyDownNoRepeat(key) => &mut key_down_no_repeat[key as usize], InputMapperMessage::KeyUpNoRepeat(key) => &mut key_up_no_repeat[key as usize], InputMapperMessage::DoubleClick(key) => &mut double_click[key as usize], + InputMapperMessage::DoubleTap(key) => &mut double_tap[key as usize], InputMapperMessage::WheelScroll => &mut wheel_scroll, InputMapperMessage::PointerMove => &mut pointer_move, InputMapperMessage::PointerShake => &mut pointer_shake, @@ -148,7 +150,7 @@ macro_rules! mapping { } )* - (key_up, key_down, key_up_no_repeat, key_down_no_repeat, double_click, wheel_scroll, pointer_move, pointer_shake) + (key_up, key_down, key_up_no_repeat, key_down_no_repeat, double_click, double_tap, wheel_scroll, pointer_move, pointer_shake) }}; } diff --git a/editor/src/messages/input_mapper/utility_types/misc.rs b/editor/src/messages/input_mapper/utility_types/misc.rs index 2470e7a719..98d103b323 100644 --- a/editor/src/messages/input_mapper/utility_types/misc.rs +++ b/editor/src/messages/input_mapper/utility_types/misc.rs @@ -12,6 +12,7 @@ pub struct Mapping { pub key_up_no_repeat: [KeyMappingEntries; NUMBER_OF_KEYS], pub key_down_no_repeat: [KeyMappingEntries; NUMBER_OF_KEYS], pub double_click: [KeyMappingEntries; NUMBER_OF_MOUSE_BUTTONS], + pub double_tap: [KeyMappingEntries; NUMBER_OF_KEYS], pub wheel_scroll: KeyMappingEntries, pub pointer_move: KeyMappingEntries, pub pointer_shake: KeyMappingEntries, @@ -36,6 +37,7 @@ impl Mapping { InputMapperMessage::KeyDownNoRepeat(key) => &self.key_down_no_repeat[*key as usize], InputMapperMessage::KeyUpNoRepeat(key) => &self.key_up_no_repeat[*key as usize], InputMapperMessage::DoubleClick(key) => &self.double_click[*key as usize], + InputMapperMessage::DoubleTap(key) => &self.double_tap[*key as usize], InputMapperMessage::WheelScroll => &self.wheel_scroll, InputMapperMessage::PointerMove => &self.pointer_move, InputMapperMessage::PointerShake => &self.pointer_shake, diff --git a/editor/src/messages/input_preprocessor/input_preprocessor_message_handler.rs b/editor/src/messages/input_preprocessor/input_preprocessor_message_handler.rs index e9f5847b7b..192f6a702a 100644 --- a/editor/src/messages/input_preprocessor/input_preprocessor_message_handler.rs +++ b/editor/src/messages/input_preprocessor/input_preprocessor_message_handler.rs @@ -1,4 +1,5 @@ use crate::application::Editor; +use crate::consts::DOUBLE_CLICK_MILLISECONDS; use crate::messages::input_mapper::utility_types::input_keyboard::{Key, KeyStates, ModifierKeys}; use crate::messages::input_mapper::utility_types::input_mouse::{MouseButton, MouseKeys, MouseState}; use crate::messages::input_mapper::utility_types::misc::FrameTimeInfo; @@ -16,6 +17,7 @@ pub struct InputPreprocessorMessageHandler { pub time: u64, pub keyboard: KeyStates, pub mouse: MouseState, + pub last_key_down: Option<(Key, u64)>, // (Key, timestamp) } #[message_handler_data] @@ -44,7 +46,21 @@ impl<'a> MessageHandler { self.update_states_of_modifier_keys(modifier_keys, responses); self.keyboard.set(key as usize); + if !key_repeat { + let is_double_tap = if let Some((last_key, last_time)) = self.last_key_down { + last_key == key && self.time.saturating_sub(last_time) < DOUBLE_CLICK_MILLISECONDS + } else { + false + }; + + if is_double_tap { + responses.add(InputMapperMessage::DoubleTap(key)); + self.last_key_down = None; + } else { + self.last_key_down = Some((key, self.time)); + } + responses.add(InputMapperMessage::KeyDownNoRepeat(key)); } responses.add(InputMapperMessage::KeyDown(key)); @@ -185,6 +201,7 @@ impl InputPreprocessorMessageHandler { #[cfg(test)] mod test { + use crate::consts::DOUBLE_CLICK_MILLISECONDS; use crate::messages::input_mapper::utility_types::input_keyboard::{Key, ModifierKeys}; use crate::messages::input_mapper::utility_types::input_mouse::{EditorMouseState, MouseKeys, ScrollDelta}; use crate::messages::prelude::*; @@ -292,4 +309,56 @@ mod test { assert!(responses.contains(&InputMapperMessage::KeyDown(Key::Control).into())); assert!(responses.contains(&InputMapperMessage::KeyDown(Key::Control).into())); } + + fn key_down(input_preprocessor: &mut InputPreprocessorMessageHandler, key: Key, responses: &mut VecDeque) { + input_preprocessor.process_message( + InputPreprocessorMessage::KeyDown { + key, + key_repeat: false, + modifier_keys: ModifierKeys::empty(), + }, + responses, + InputPreprocessorMessageContext { + viewport: &ViewportMessageHandler::default(), + }, + ); + } + + #[test] + fn process_double_tap_within_threshold() { + let mut input_preprocessor = InputPreprocessorMessageHandler::default(); + let mut responses = VecDeque::new(); + + // First tap at time 0 + key_down(&mut input_preprocessor, Key::Space, &mut responses); + responses.clear(); + + // Second tap within threshold + input_preprocessor.time = 50; + key_down(&mut input_preprocessor, Key::Space, &mut responses); + + assert!(responses.contains(&InputMapperMessage::DoubleTap(Key::Space).into())); + assert!(responses.contains(&InputMapperMessage::KeyDown(Key::Space).into())); + assert!(responses.contains(&InputMapperMessage::KeyDownNoRepeat(Key::Space).into())); + assert!(input_preprocessor.last_key_down.is_none()); + } + + #[test] + fn process_double_tap_outside_threshold() { + let mut input_preprocessor = InputPreprocessorMessageHandler::default(); + let mut responses = VecDeque::new(); + + // First tap at time 0 + key_down(&mut input_preprocessor, Key::Space, &mut responses); + responses.clear(); + + // Second tap outside threshold + input_preprocessor.time = DOUBLE_CLICK_MILLISECONDS + 1; + key_down(&mut input_preprocessor, Key::Space, &mut responses); + + assert!(!responses.contains(&InputMapperMessage::DoubleTap(Key::Space).into())); + assert!(responses.contains(&InputMapperMessage::KeyDown(Key::Space).into())); + assert!(responses.contains(&InputMapperMessage::KeyDownNoRepeat(Key::Space).into())); + assert_eq!(input_preprocessor.last_key_down, Some((Key::Space, DOUBLE_CLICK_MILLISECONDS + 1))); + } }