From 1b99932f1fea039c1269e97810f16dd6643fd959 Mon Sep 17 00:00:00 2001 From: Kulratan Thapar Date: Wed, 18 Feb 2026 10:05:03 +0000 Subject: [PATCH 01/12] Double-Tap --- .../input_mapper/input_mapper_message.rs | 2 + .../input_mapper_message_handler.rs | 3 +- .../messages/input_mapper/input_mappings.rs | 9 ++- .../input_mapper/utility_types/macros.rs | 4 +- .../input_mapper/utility_types/misc.rs | 2 + .../input_preprocessor_message_handler.rs | 62 +++++++++++++++++++ 6 files changed, 78 insertions(+), 4 deletions(-) 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 2d9669a355..ae054b698d 100644 --- a/editor/src/messages/input_mapper/input_mappings.rs +++ b/editor/src/messages/input_mapper/input_mappings.rs @@ -338,7 +338,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), @@ -466,7 +466,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` @@ -479,6 +479,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` @@ -492,6 +496,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 e98df2a22e..76616ee773 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..b390659cda 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,18 @@ impl<'a> MessageHandler { self.update_states_of_modifier_keys(modifier_keys, responses); self.keyboard.set(key as usize); + if !key_repeat { + if let Some((last_key, last_time)) = self.last_key_down { + if last_key == key && self.time.saturating_sub(last_time) < DOUBLE_CLICK_MILLISECONDS { + responses.add(InputMapperMessage::DoubleTap(key)); + self.last_key_down = None; + } else { + self.last_key_down = Some((key, self.time)); + } + } else { + self.last_key_down = Some((key, self.time)); + } responses.add(InputMapperMessage::KeyDownNoRepeat(key)); } responses.add(InputMapperMessage::KeyDown(key)); @@ -185,6 +198,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 +306,52 @@ 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!(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_eq!(input_preprocessor.last_key_down, Some((Key::Space, DOUBLE_CLICK_MILLISECONDS + 1))); + } } From 955b9dea461f0a9956c80c24395cf650baff84e8 Mon Sep 17 00:00:00 2001 From: Kulratan Thapar Date: Wed, 18 Feb 2026 10:19:37 +0000 Subject: [PATCH 02/12] improvement --- .../input_preprocessor_message_handler.rs | 24 ++++++++++++------- 1 file changed, 15 insertions(+), 9 deletions(-) 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 b390659cda..c036ff7a29 100644 --- a/editor/src/messages/input_preprocessor/input_preprocessor_message_handler.rs +++ b/editor/src/messages/input_preprocessor/input_preprocessor_message_handler.rs @@ -48,19 +48,23 @@ impl<'a> MessageHandler { self.update_states_of_modifier_keys(modifier_keys, responses); @@ -335,6 +339,8 @@ mod test { 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()); } From ae40ca6d1de9e95db2d21d566ca83842b816d5d0 Mon Sep 17 00:00:00 2001 From: Kulratan Thapar Date: Wed, 18 Feb 2026 10:28:52 +0000 Subject: [PATCH 03/12] Fixed --- .../input_preprocessor_message_handler.rs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) 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 c036ff7a29..3a3d0e861f 100644 --- a/editor/src/messages/input_preprocessor/input_preprocessor_message_handler.rs +++ b/editor/src/messages/input_preprocessor/input_preprocessor_message_handler.rs @@ -59,12 +59,11 @@ impl<'a> MessageHandler { self.update_states_of_modifier_keys(modifier_keys, responses); @@ -339,8 +338,8 @@ mod test { 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!(responses.contains(&InputMapperMessage::KeyDown(Key::Space).into())); + assert!(responses.contains(&InputMapperMessage::KeyDownNoRepeat(Key::Space).into())); assert!(input_preprocessor.last_key_down.is_none()); } From 1c0f090944ecb97766ffc4d38973fa3c574826b4 Mon Sep 17 00:00:00 2001 From: Kulratan Thapar Date: Wed, 18 Feb 2026 11:36:47 +0000 Subject: [PATCH 04/12] Test --- .../input_preprocessor/input_preprocessor_message_handler.rs | 2 ++ 1 file changed, 2 insertions(+) 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 3a3d0e861f..192f6a702a 100644 --- a/editor/src/messages/input_preprocessor/input_preprocessor_message_handler.rs +++ b/editor/src/messages/input_preprocessor/input_preprocessor_message_handler.rs @@ -357,6 +357,8 @@ mod test { 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))); } } From d6450901942a130973cf2ede64d1bec1c023e764 Mon Sep 17 00:00:00 2001 From: Kulratan Thapar Date: Sun, 22 Feb 2026 06:41:34 +0000 Subject: [PATCH 05/12] Update --- .../input_preprocessor_message_handler.rs | 47 ++++++++++++++++--- 1 file changed, 40 insertions(+), 7 deletions(-) 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 192f6a702a..c7dece889a 100644 --- a/editor/src/messages/input_preprocessor/input_preprocessor_message_handler.rs +++ b/editor/src/messages/input_preprocessor/input_preprocessor_message_handler.rs @@ -18,6 +18,7 @@ pub struct InputPreprocessorMessageHandler { pub keyboard: KeyStates, pub mouse: MouseState, pub last_key_down: Option<(Key, u64)>, // (Key, timestamp) + pub double_tap_key: Option, } #[message_handler_data] @@ -48,14 +49,12 @@ impl<'a> MessageHandler MessageHandler { @@ -324,6 +327,20 @@ mod test { ); } + fn key_up(input_preprocessor: &mut InputPreprocessorMessageHandler, key: Key, responses: &mut VecDeque) { + input_preprocessor.process_message( + InputPreprocessorMessage::KeyUp { + 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(); @@ -331,16 +348,25 @@ mod test { // First tap at time 0 key_down(&mut input_preprocessor, Key::Space, &mut responses); + key_up(&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::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()); + assert_eq!(input_preprocessor.double_tap_key, Some(Key::Space)); + + responses.clear(); + key_up(&mut input_preprocessor, Key::Space, &mut responses); + + assert!(responses.contains(&InputMapperMessage::DoubleTap(Key::Space).into())); + assert!(input_preprocessor.last_key_down.is_none()); + assert!(input_preprocessor.double_tap_key.is_none()); } #[test] @@ -350,6 +376,7 @@ mod test { // First tap at time 0 key_down(&mut input_preprocessor, Key::Space, &mut responses); + key_up(&mut input_preprocessor, Key::Space, &mut responses); responses.clear(); // Second tap outside threshold @@ -360,5 +387,11 @@ mod test { 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))); + assert!(input_preprocessor.double_tap_key.is_none()); + + responses.clear(); + key_up(&mut input_preprocessor, Key::Space, &mut responses); + + assert!(!responses.contains(&InputMapperMessage::DoubleTap(Key::Space).into())); } } From 57213f8d1e5482b5105632796f0c0927407c7750 Mon Sep 17 00:00:00 2001 From: Kulratan Thapar Date: Sun, 22 Feb 2026 07:59:08 +0000 Subject: [PATCH 06/12] Fix --- .../input_preprocessor_message_handler.rs | 73 +++++++++++++++++-- 1 file changed, 68 insertions(+), 5 deletions(-) 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 c7dece889a..64319b9455 100644 --- a/editor/src/messages/input_preprocessor/input_preprocessor_message_handler.rs +++ b/editor/src/messages/input_preprocessor/input_preprocessor_message_handler.rs @@ -18,7 +18,7 @@ pub struct InputPreprocessorMessageHandler { pub keyboard: KeyStates, pub mouse: MouseState, pub last_key_down: Option<(Key, u64)>, // (Key, timestamp) - pub double_tap_key: Option, + pub double_tap_key: Option<(Key, u64)>, } #[message_handler_data] @@ -28,6 +28,7 @@ impl<'a> MessageHandler { + self.clear_double_tap_state(); self.update_states_of_modifier_keys(modifier_keys, responses); let mouse_state = editor_mouse_state.to_mouse_state(viewport); @@ -54,10 +55,11 @@ impl<'a> MessageHandler MessageHandler { + self.clear_double_tap_state(); self.update_states_of_modifier_keys(modifier_keys, responses); let mouse_state = editor_mouse_state.to_mouse_state(viewport); @@ -96,6 +101,7 @@ impl<'a> MessageHandler { + self.clear_double_tap_state(); self.update_states_of_modifier_keys(modifier_keys, responses); let mouse_state = editor_mouse_state.to_mouse_state(viewport); @@ -117,6 +123,7 @@ impl<'a> MessageHandler { + self.clear_double_tap_state(); self.update_states_of_modifier_keys(modifier_keys, responses); let mouse_state = editor_mouse_state.to_mouse_state(viewport); @@ -135,6 +142,11 @@ impl<'a> MessageHandler) { let click_mappings = [ (MouseKeys::LEFT, Key::MouseLeft), @@ -359,7 +371,7 @@ mod test { assert!(responses.contains(&InputMapperMessage::KeyDown(Key::Space).into())); assert!(responses.contains(&InputMapperMessage::KeyDownNoRepeat(Key::Space).into())); assert!(input_preprocessor.last_key_down.is_none()); - assert_eq!(input_preprocessor.double_tap_key, Some(Key::Space)); + assert_eq!(input_preprocessor.double_tap_key, Some((Key::Space, 50))); responses.clear(); key_up(&mut input_preprocessor, Key::Space, &mut responses); @@ -394,4 +406,55 @@ mod test { assert!(!responses.contains(&InputMapperMessage::DoubleTap(Key::Space).into())); } + + #[test] + fn process_double_tap_held_too_long() { + let mut input_preprocessor = InputPreprocessorMessageHandler::default(); + let mut responses = VecDeque::new(); + + key_down(&mut input_preprocessor, Key::Space, &mut responses); + key_up(&mut input_preprocessor, Key::Space, &mut responses); + responses.clear(); + + input_preprocessor.time = 50; + key_down(&mut input_preprocessor, Key::Space, &mut responses); + responses.clear(); + + // Release after the threshold + input_preprocessor.time = 50 + DOUBLE_CLICK_MILLISECONDS + 1; + key_up(&mut input_preprocessor, Key::Space, &mut responses); + + assert!(!responses.contains(&InputMapperMessage::DoubleTap(Key::Space).into())); + } + + #[test] + fn process_double_tap_interrupted_by_pointer() { + let mut input_preprocessor = InputPreprocessorMessageHandler::default(); + let mut responses = VecDeque::new(); + + key_down(&mut input_preprocessor, Key::Space, &mut responses); + key_up(&mut input_preprocessor, Key::Space, &mut responses); + responses.clear(); + + input_preprocessor.time = 50; + key_down(&mut input_preprocessor, Key::Space, &mut responses); + responses.clear(); + + // PointerDown happens! + input_preprocessor.process_message( + InputPreprocessorMessage::PointerDown { + editor_mouse_state: EditorMouseState::default(), + modifier_keys: ModifierKeys::empty(), + }, + &mut responses, + InputPreprocessorMessageContext { + viewport: &ViewportMessageHandler::default(), + }, + ); + responses.clear(); + + key_up(&mut input_preprocessor, Key::Space, &mut responses); + + assert!(!responses.contains(&InputMapperMessage::DoubleTap(Key::Space).into())); + } } From f2e07ded926925ffd3e88241d182f0d3665512b5 Mon Sep 17 00:00:00 2001 From: Kulratan Thapar Date: Sun, 22 Feb 2026 08:27:35 +0000 Subject: [PATCH 07/12] Fix --- .../input_preprocessor_message_handler.rs | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) 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 64319b9455..7820fa72f0 100644 --- a/editor/src/messages/input_preprocessor/input_preprocessor_message_handler.rs +++ b/editor/src/messages/input_preprocessor/input_preprocessor_message_handler.rs @@ -90,6 +90,7 @@ impl<'a> MessageHandler { + self.clear_double_tap_state(); self.update_states_of_modifier_keys(modifier_keys, responses); let mouse_state = editor_mouse_state.to_mouse_state(viewport); @@ -457,4 +458,35 @@ mod test { assert!(!responses.contains(&InputMapperMessage::DoubleTap(Key::Space).into())); } + + #[test] + fn process_double_tap_interrupted_by_mouse_movement() { + let mut input_preprocessor = InputPreprocessorMessageHandler::default(); + let mut responses = VecDeque::new(); + + key_down(&mut input_preprocessor, Key::Space, &mut responses); + key_up(&mut input_preprocessor, Key::Space, &mut responses); + responses.clear(); + + input_preprocessor.time = 50; + key_down(&mut input_preprocessor, Key::Space, &mut responses); + responses.clear(); + + // PointerMove happens! + input_preprocessor.process_message( + InputPreprocessorMessage::PointerMove { + editor_mouse_state: EditorMouseState::default(), + modifier_keys: ModifierKeys::empty(), + }, + &mut responses, + InputPreprocessorMessageContext { + viewport: &ViewportMessageHandler::default(), + }, + ); + responses.clear(); + + key_up(&mut input_preprocessor, Key::Space, &mut responses); + + assert!(!responses.contains(&InputMapperMessage::DoubleTap(Key::Space).into())); + } } From a75fc4071fc283c09c2948f1c7cefe05dd5b0640 Mon Sep 17 00:00:00 2001 From: Kulratan Thapar Date: Mon, 23 Feb 2026 06:53:24 +0000 Subject: [PATCH 08/12] Shortcut --- .../input_mapper/input_mapper_message_handler.rs | 10 +++++++++- .../input_mapper/utility_types/input_keyboard.rs | 3 +++ .../input_mapper/utility_types/input_mouse.rs | 13 +++++++++++++ 3 files changed, 25 insertions(+), 1 deletion(-) 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 50744c5820..17eac79e7e 100644 --- a/editor/src/messages/input_mapper/input_mapper_message_handler.rs +++ b/editor/src/messages/input_mapper/input_mapper_message_handler.rs @@ -69,7 +69,15 @@ 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) | IMM::DoubleTap(key) => keys.push(key), + IMM::KeyDown(key) | IMM::KeyUp(key) | IMM::KeyDownNoRepeat(key) | IMM::KeyUpNoRepeat(key) => keys.push(key), + IMM::DoubleTap(key) => { + keys.push(Key::Double); + keys.push(key); + } + IMM::DoubleClick(button) => { + keys.push(Key::Double); + keys.push(button.into()); + } _ => (), } diff --git a/editor/src/messages/input_mapper/utility_types/input_keyboard.rs b/editor/src/messages/input_mapper/utility_types/input_keyboard.rs index b6a2424c52..aa541a605f 100644 --- a/editor/src/messages/input_mapper/utility_types/input_keyboard.rs +++ b/editor/src/messages/input_mapper/utility_types/input_keyboard.rs @@ -240,6 +240,8 @@ pub enum Key { FakeKeyPlus, /// Not a physical key that can be pressed. May be used so that an actual shortcut bound to all ten number keys (0, ..., 9) can separately map this fake "key" as an additional binding to display the "0–9" shortcut label in the UI. FakeKeyNumbers, + /// Not a physical key that can be pressed. + Double, _KeysVariantCount, // This has to be the last element in the enum } @@ -331,6 +333,7 @@ impl fmt::Display for Key { // Fake keys for displaying special labels in the UI Self::FakeKeyPlus => "+", Self::FakeKeyNumbers => "0–9", + Self::Double => "2x", _ => key_name.as_str(), }; diff --git a/editor/src/messages/input_mapper/utility_types/input_mouse.rs b/editor/src/messages/input_mapper/utility_types/input_mouse.rs index 420a3bdd46..9278430847 100644 --- a/editor/src/messages/input_mapper/utility_types/input_mouse.rs +++ b/editor/src/messages/input_mapper/utility_types/input_mouse.rs @@ -1,3 +1,4 @@ +use super::input_keyboard::Key; use crate::consts::DRAG_THRESHOLD; use crate::messages::prelude::*; use bitflags::bitflags; @@ -121,4 +122,16 @@ pub enum MouseButton { Forward, } +impl From for Key { + fn from(mouse_button: MouseButton) -> Self { + match mouse_button { + MouseButton::Left => Key::MouseLeft, + MouseButton::Right => Key::MouseRight, + MouseButton::Middle => Key::MouseMiddle, + MouseButton::Back => Key::MouseBack, + MouseButton::Forward => Key::MouseForward, + } + } +} + pub const NUMBER_OF_MOUSE_BUTTONS: usize = 5; // Should be the number of variants in MouseButton From c7ba070f1faaf75259e761357fc7adfd61b89f72 Mon Sep 17 00:00:00 2001 From: Kulratan Thapar Date: Mon, 23 Feb 2026 19:53:10 +0000 Subject: [PATCH 09/12] Changes --- .../input_preprocessor_message_handler.rs | 77 +++++++++++-------- 1 file changed, 46 insertions(+), 31 deletions(-) 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 7820fa72f0..72fac0fb83 100644 --- a/editor/src/messages/input_preprocessor/input_preprocessor_message_handler.rs +++ b/editor/src/messages/input_preprocessor/input_preprocessor_message_handler.rs @@ -17,7 +17,9 @@ pub struct InputPreprocessorMessageHandler { pub time: u64, pub keyboard: KeyStates, pub mouse: MouseState, - pub last_key_down: Option<(Key, u64)>, // (Key, timestamp) + /// The most recent non-repeated key press and the timestamp of when it occurred, used as the first tap in double-tap detection. + pub last_key_down: Option<(Key, u64)>, + /// Set when a second tap of the same key occurs within the double-tap threshold. Cleared by any interrupting input (mouse button, scroll, or different key). The `DoubleTap` event is emitted on `KeyUp` if this is still set. pub double_tap_key: Option<(Key, u64)>, } @@ -50,11 +52,12 @@ impl<'a> MessageHandler MessageHandler MessageHandler { - self.clear_double_tap_state(); self.update_states_of_modifier_keys(modifier_keys, responses); let mouse_state = editor_mouse_state.to_mouse_state(viewport); @@ -354,31 +355,35 @@ mod test { ); } + fn process_input(input_preprocessor: &mut InputPreprocessorMessageHandler, message: InputPreprocessorMessage, responses: &mut VecDeque) { + input_preprocessor.process_message( + message, + 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); key_up(&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()); assert_eq!(input_preprocessor.double_tap_key, Some((Key::Space, 50))); responses.clear(); key_up(&mut input_preprocessor, Key::Space, &mut responses); assert!(responses.contains(&InputMapperMessage::DoubleTap(Key::Space).into())); - assert!(input_preprocessor.last_key_down.is_none()); assert!(input_preprocessor.double_tap_key.is_none()); } @@ -387,19 +392,14 @@ mod test { 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); key_up(&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))); assert!(input_preprocessor.double_tap_key.is_none()); responses.clear(); @@ -421,7 +421,6 @@ mod test { key_down(&mut input_preprocessor, Key::Space, &mut responses); responses.clear(); - // Release after the threshold input_preprocessor.time = 50 + DOUBLE_CLICK_MILLISECONDS + 1; key_up(&mut input_preprocessor, Key::Space, &mut responses); @@ -429,7 +428,7 @@ mod test { } #[test] - fn process_double_tap_interrupted_by_pointer() { + fn process_double_tap_interrupted_by_pointer_down() { let mut input_preprocessor = InputPreprocessorMessageHandler::default(); let mut responses = VecDeque::new(); @@ -441,16 +440,13 @@ mod test { key_down(&mut input_preprocessor, Key::Space, &mut responses); responses.clear(); - // PointerDown happens! - input_preprocessor.process_message( + process_input( + &mut input_preprocessor, InputPreprocessorMessage::PointerDown { editor_mouse_state: EditorMouseState::default(), modifier_keys: ModifierKeys::empty(), }, &mut responses, - InputPreprocessorMessageContext { - viewport: &ViewportMessageHandler::default(), - }, ); responses.clear(); @@ -460,7 +456,7 @@ mod test { } #[test] - fn process_double_tap_interrupted_by_mouse_movement() { + fn process_double_tap_not_interrupted_by_mouse_movement() { let mut input_preprocessor = InputPreprocessorMessageHandler::default(); let mut responses = VecDeque::new(); @@ -472,21 +468,40 @@ mod test { key_down(&mut input_preprocessor, Key::Space, &mut responses); responses.clear(); - // PointerMove happens! - input_preprocessor.process_message( + process_input( + &mut input_preprocessor, InputPreprocessorMessage::PointerMove { editor_mouse_state: EditorMouseState::default(), modifier_keys: ModifierKeys::empty(), }, &mut responses, - InputPreprocessorMessageContext { - viewport: &ViewportMessageHandler::default(), - }, ); responses.clear(); key_up(&mut input_preprocessor, Key::Space, &mut responses); + assert!(responses.contains(&InputMapperMessage::DoubleTap(Key::Space).into())); + } + + #[test] + fn process_double_tap_blocked_by_mouse_button_held() { + let mut input_preprocessor = InputPreprocessorMessageHandler::default(); + let mut responses = VecDeque::new(); + + input_preprocessor.mouse.mouse_keys = MouseKeys::LEFT; + + key_down(&mut input_preprocessor, Key::Space, &mut responses); + key_up(&mut input_preprocessor, Key::Space, &mut responses); + responses.clear(); + + input_preprocessor.time = 50; + key_down(&mut input_preprocessor, Key::Space, &mut responses); + + assert!(input_preprocessor.double_tap_key.is_none()); + + responses.clear(); + key_up(&mut input_preprocessor, Key::Space, &mut responses); + assert!(!responses.contains(&InputMapperMessage::DoubleTap(Key::Space).into())); } } From cea19891b893d895dc5412e1c9bcbe64c9256497 Mon Sep 17 00:00:00 2001 From: Kulratan Thapar Date: Tue, 24 Feb 2026 08:10:22 +0000 Subject: [PATCH 10/12] Remove double-tap tests (moved to separate PR) and add Ctrl+Space for Focus Document --- .../messages/input_mapper/input_mappings.rs | 1 + .../input_preprocessor_message_handler.rs | 181 +----------------- 2 files changed, 2 insertions(+), 180 deletions(-) diff --git a/editor/src/messages/input_mapper/input_mappings.rs b/editor/src/messages/input_mapper/input_mappings.rs index ae054b698d..3923a4e990 100644 --- a/editor/src/messages/input_mapper/input_mappings.rs +++ b/editor/src/messages/input_mapper/input_mappings.rs @@ -452,6 +452,7 @@ pub fn input_mappings(zoom_with_scroll: bool) -> Mapping { entry!(KeyDown(KeyR); modifiers=[Alt], action_dispatch=PortfolioMessage::ToggleRulers), entry!(KeyDown(KeyD); modifiers=[Alt], action_dispatch=PortfolioMessage::ToggleDataPanelOpen), entry!(KeyDown(Enter); modifiers=[Alt], action_dispatch=PortfolioMessage::ToggleFocusDocument), + entry!(KeyDown(Space); modifiers=[Accel], action_dispatch=PortfolioMessage::ToggleFocusDocument), // // DialogMessage entry!(KeyDown(KeyE); modifiers=[Accel], action_dispatch=DialogMessage::RequestExportDialog), 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 72fac0fb83..943524254a 100644 --- a/editor/src/messages/input_preprocessor/input_preprocessor_message_handler.rs +++ b/editor/src/messages/input_preprocessor/input_preprocessor_message_handler.rs @@ -218,9 +218,8 @@ 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::input_mapper::utility_types::input_mouse::{EditorMouseState, ScrollDelta}; use crate::messages::prelude::*; #[test] @@ -326,182 +325,4 @@ 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(), - }, - ); - } - - fn key_up(input_preprocessor: &mut InputPreprocessorMessageHandler, key: Key, responses: &mut VecDeque) { - input_preprocessor.process_message( - InputPreprocessorMessage::KeyUp { - key, - key_repeat: false, - modifier_keys: ModifierKeys::empty(), - }, - responses, - InputPreprocessorMessageContext { - viewport: &ViewportMessageHandler::default(), - }, - ); - } - - fn process_input(input_preprocessor: &mut InputPreprocessorMessageHandler, message: InputPreprocessorMessage, responses: &mut VecDeque) { - input_preprocessor.process_message( - message, - responses, - InputPreprocessorMessageContext { - viewport: &ViewportMessageHandler::default(), - }, - ); - } - - #[test] - fn process_double_tap_within_threshold() { - let mut input_preprocessor = InputPreprocessorMessageHandler::default(); - let mut responses = VecDeque::new(); - - key_down(&mut input_preprocessor, Key::Space, &mut responses); - key_up(&mut input_preprocessor, Key::Space, &mut responses); - responses.clear(); - - input_preprocessor.time = 50; - key_down(&mut input_preprocessor, Key::Space, &mut responses); - - assert!(!responses.contains(&InputMapperMessage::DoubleTap(Key::Space).into())); - assert_eq!(input_preprocessor.double_tap_key, Some((Key::Space, 50))); - - responses.clear(); - key_up(&mut input_preprocessor, Key::Space, &mut responses); - - assert!(responses.contains(&InputMapperMessage::DoubleTap(Key::Space).into())); - assert!(input_preprocessor.double_tap_key.is_none()); - } - - #[test] - fn process_double_tap_outside_threshold() { - let mut input_preprocessor = InputPreprocessorMessageHandler::default(); - let mut responses = VecDeque::new(); - - key_down(&mut input_preprocessor, Key::Space, &mut responses); - key_up(&mut input_preprocessor, Key::Space, &mut responses); - responses.clear(); - - 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!(input_preprocessor.double_tap_key.is_none()); - - responses.clear(); - key_up(&mut input_preprocessor, Key::Space, &mut responses); - - assert!(!responses.contains(&InputMapperMessage::DoubleTap(Key::Space).into())); - } - - #[test] - fn process_double_tap_held_too_long() { - let mut input_preprocessor = InputPreprocessorMessageHandler::default(); - let mut responses = VecDeque::new(); - - key_down(&mut input_preprocessor, Key::Space, &mut responses); - key_up(&mut input_preprocessor, Key::Space, &mut responses); - responses.clear(); - - input_preprocessor.time = 50; - key_down(&mut input_preprocessor, Key::Space, &mut responses); - responses.clear(); - - input_preprocessor.time = 50 + DOUBLE_CLICK_MILLISECONDS + 1; - key_up(&mut input_preprocessor, Key::Space, &mut responses); - - assert!(!responses.contains(&InputMapperMessage::DoubleTap(Key::Space).into())); - } - - #[test] - fn process_double_tap_interrupted_by_pointer_down() { - let mut input_preprocessor = InputPreprocessorMessageHandler::default(); - let mut responses = VecDeque::new(); - - key_down(&mut input_preprocessor, Key::Space, &mut responses); - key_up(&mut input_preprocessor, Key::Space, &mut responses); - responses.clear(); - - input_preprocessor.time = 50; - key_down(&mut input_preprocessor, Key::Space, &mut responses); - responses.clear(); - - process_input( - &mut input_preprocessor, - InputPreprocessorMessage::PointerDown { - editor_mouse_state: EditorMouseState::default(), - modifier_keys: ModifierKeys::empty(), - }, - &mut responses, - ); - responses.clear(); - - key_up(&mut input_preprocessor, Key::Space, &mut responses); - - assert!(!responses.contains(&InputMapperMessage::DoubleTap(Key::Space).into())); - } - - #[test] - fn process_double_tap_not_interrupted_by_mouse_movement() { - let mut input_preprocessor = InputPreprocessorMessageHandler::default(); - let mut responses = VecDeque::new(); - - key_down(&mut input_preprocessor, Key::Space, &mut responses); - key_up(&mut input_preprocessor, Key::Space, &mut responses); - responses.clear(); - - input_preprocessor.time = 50; - key_down(&mut input_preprocessor, Key::Space, &mut responses); - responses.clear(); - - process_input( - &mut input_preprocessor, - InputPreprocessorMessage::PointerMove { - editor_mouse_state: EditorMouseState::default(), - modifier_keys: ModifierKeys::empty(), - }, - &mut responses, - ); - responses.clear(); - - key_up(&mut input_preprocessor, Key::Space, &mut responses); - - assert!(responses.contains(&InputMapperMessage::DoubleTap(Key::Space).into())); - } - - #[test] - fn process_double_tap_blocked_by_mouse_button_held() { - let mut input_preprocessor = InputPreprocessorMessageHandler::default(); - let mut responses = VecDeque::new(); - - input_preprocessor.mouse.mouse_keys = MouseKeys::LEFT; - - key_down(&mut input_preprocessor, Key::Space, &mut responses); - key_up(&mut input_preprocessor, Key::Space, &mut responses); - responses.clear(); - - input_preprocessor.time = 50; - key_down(&mut input_preprocessor, Key::Space, &mut responses); - - assert!(input_preprocessor.double_tap_key.is_none()); - - responses.clear(); - key_up(&mut input_preprocessor, Key::Space, &mut responses); - - assert!(!responses.contains(&InputMapperMessage::DoubleTap(Key::Space).into())); - } } From 9d6c366b3b5a7e1d6091b765b2958bceccf33e50 Mon Sep 17 00:00:00 2001 From: Kulratan Thapar Date: Tue, 24 Feb 2026 08:20:44 +0000 Subject: [PATCH 11/12] check whether modifier keys are held --- .../input_preprocessor/input_preprocessor_message_handler.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 943524254a..bdfa3b3b08 100644 --- a/editor/src/messages/input_preprocessor/input_preprocessor_message_handler.rs +++ b/editor/src/messages/input_preprocessor/input_preprocessor_message_handler.rs @@ -53,11 +53,12 @@ impl<'a> MessageHandler Date: Tue, 24 Feb 2026 11:33:17 +0000 Subject: [PATCH 12/12] Fixed imports --- .../input_preprocessor/input_preprocessor_message_handler.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 bdfa3b3b08..72d3efb8d5 100644 --- a/editor/src/messages/input_preprocessor/input_preprocessor_message_handler.rs +++ b/editor/src/messages/input_preprocessor/input_preprocessor_message_handler.rs @@ -220,7 +220,7 @@ impl InputPreprocessorMessageHandler { #[cfg(test)] mod test { use crate::messages::input_mapper::utility_types::input_keyboard::{Key, ModifierKeys}; - use crate::messages::input_mapper::utility_types::input_mouse::{EditorMouseState, ScrollDelta}; + use crate::messages::input_mapper::utility_types::input_mouse::{EditorMouseState, MouseKeys, ScrollDelta}; use crate::messages::prelude::*; #[test]