From 88b230143a7032e951f0a3699da137ba4adde835 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 8023d0f926..520bd4aa57 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 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 c7baa0dc57fa655184232cfcf6488f3b86a97e6c 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 903fae75e12acfbaea4658789b7396070bfa9a64 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 79c1c8204b8f67fc4d5cbaa245faa56504d00171 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 f6720b83ffe02182bbc29dad67193f3fbf614b4e 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 1642688f50e31e9eedaae3dadf962b69d72fddc3 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 66f57158c0782bd235789ff86dc1ca5a437042eb 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 a2d4c2d7764d860a7d37c832553ebd691a39fb3a 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 041edb4ca0..083271b47e 100644 --- a/editor/src/messages/input_mapper/utility_types/input_keyboard.rs +++ b/editor/src/messages/input_mapper/utility_types/input_keyboard.rs @@ -239,6 +239,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 } @@ -330,6 +332,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 6b3f4bb421726892cd0dc387d20aa794ed57c87f 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 614102b2c7481e764af634081c4321c3f92b736a 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 520bd4aa57..a0d95dec1a 100644 --- a/editor/src/messages/input_mapper/input_mappings.rs +++ b/editor/src/messages/input_mapper/input_mappings.rs @@ -449,6 +449,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 02a22abd92d8e15ccae2f42478b5d71532f0988f 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]