diff --git a/core/src/input.rs b/core/src/input.rs index 921c5687e76c..fc7e0c449d16 100644 --- a/core/src/input.rs +++ b/core/src/input.rs @@ -72,7 +72,10 @@ impl ClickEventData { } pub struct InputManager { - key_descriptors_down: HashSet, + /// Tracks which physical keys (identified by physical key + location) are currently pressed. + /// This allows us to match key-up events even when the logical key changes + /// due to modifier state changes (e.g., shift pressed/released). + keys_down_phys_loc: HashSet<(PhysicalKey, KeyLocation)>, keys_down: HashSet, keys_toggled: HashSet, @@ -89,7 +92,7 @@ pub struct InputManager { impl InputManager { pub fn new(gamepad_button_mapping: HashMap) -> Self { Self { - key_descriptors_down: HashSet::new(), + keys_down_phys_loc: HashSet::new(), keys_down: HashSet::new(), keys_toggled: HashSet::new(), last_key: KeyCode::UNKNOWN, @@ -156,11 +159,16 @@ impl InputManager { } PlayerEvent::KeyDown { key } => { - self.key_descriptors_down.insert(key); + // Track the physical key + location, so we can match key-up events + // even when the logical key changes due to modifier state changes + // (e.g., shift pressed/released). + self.keys_down_phys_loc + .insert((key.physical_key, key.key_location)); let key_code = self.map_to_key_code(key)?; let key_char = self.map_to_key_char(key); let key_location = self.map_to_key_location(key); + InputEvent::KeyDown { key_code, key_char, @@ -168,10 +176,17 @@ impl InputManager { } } PlayerEvent::KeyUp { key } => { - if !self.key_descriptors_down.remove(&key) { + // Match key-up events by physical key and location, not the exact + // KeyDescriptor. This handles cases where modifier state changes + // between key-down and key-up (e.g., shift pressed/released), + // which would change the logical key (e.g., 'w' -> 'W'). + if !self + .keys_down_phys_loc + .remove(&(key.physical_key, key.key_location)) + { // Ignore spurious KeyUp events that may happen e.g. during IME. // We assume that in order for a key to generate KeyUp, it had to - // generate KeyDown for the same exact KeyDescriptor. + // generate KeyDown for the same physical key and location. // TODO Apparently this behavior is platform-dependent and // doesn't happen on Windows. We cannot remove it fully @@ -183,6 +198,7 @@ impl InputManager { let key_code = self.map_to_key_code(key)?; let key_char = self.map_to_key_char(key); let key_location = self.map_to_key_location(key); + InputEvent::KeyUp { key_code, key_char, diff --git a/tests/framework/src/runner/automation.rs b/tests/framework/src/runner/automation.rs index c8b55400b0d8..8b149f51eb87 100644 --- a/tests/framework/src/runner/automation.rs +++ b/tests/framework/src/runner/automation.rs @@ -117,53 +117,149 @@ pub fn perform_automated_event(evt: &AutomatedEvent, player: &mut Player) { } pub fn automated_key_to_descriptor(automated_key: AutomatedKey) -> KeyDescriptor { - let logical_key = match automated_key { - AutomatedKey::Char(ch) | AutomatedKey::Numpad(ch) => LogicalKey::Character(ch), - AutomatedKey::ArrowDown => LogicalKey::Named(NamedKey::ArrowDown), - AutomatedKey::ArrowLeft => LogicalKey::Named(NamedKey::ArrowLeft), - AutomatedKey::ArrowRight => LogicalKey::Named(NamedKey::ArrowRight), - AutomatedKey::ArrowUp => LogicalKey::Named(NamedKey::ArrowUp), - AutomatedKey::Backspace => LogicalKey::Named(NamedKey::Backspace), - AutomatedKey::CapsLock => LogicalKey::Named(NamedKey::CapsLock), - AutomatedKey::Delete => LogicalKey::Named(NamedKey::Delete), - AutomatedKey::End => LogicalKey::Named(NamedKey::End), - AutomatedKey::Enter => LogicalKey::Named(NamedKey::Enter), - AutomatedKey::Escape => LogicalKey::Named(NamedKey::Escape), - AutomatedKey::F1 => LogicalKey::Named(NamedKey::F1), - AutomatedKey::F2 => LogicalKey::Named(NamedKey::F2), - AutomatedKey::F3 => LogicalKey::Named(NamedKey::F3), - AutomatedKey::F4 => LogicalKey::Named(NamedKey::F4), - AutomatedKey::F5 => LogicalKey::Named(NamedKey::F5), - AutomatedKey::F6 => LogicalKey::Named(NamedKey::F6), - AutomatedKey::F7 => LogicalKey::Named(NamedKey::F7), - AutomatedKey::F8 => LogicalKey::Named(NamedKey::F8), - AutomatedKey::F9 => LogicalKey::Named(NamedKey::F9), - AutomatedKey::Home => LogicalKey::Named(NamedKey::Home), - AutomatedKey::Insert => LogicalKey::Named(NamedKey::Insert), - AutomatedKey::LeftAlt => LogicalKey::Named(NamedKey::Alt), - AutomatedKey::LeftControl => LogicalKey::Named(NamedKey::Control), - AutomatedKey::LeftShift => LogicalKey::Named(NamedKey::Shift), - AutomatedKey::NumLock => LogicalKey::Named(NamedKey::NumLock), - AutomatedKey::NumpadDelete => LogicalKey::Named(NamedKey::Delete), - AutomatedKey::NumpadDown => LogicalKey::Named(NamedKey::ArrowDown), - AutomatedKey::NumpadEnd => LogicalKey::Named(NamedKey::End), - AutomatedKey::NumpadHome => LogicalKey::Named(NamedKey::Home), - AutomatedKey::NumpadInsert => LogicalKey::Named(NamedKey::Insert), - AutomatedKey::NumpadLeft => LogicalKey::Named(NamedKey::ArrowLeft), - AutomatedKey::NumpadPageDown => LogicalKey::Named(NamedKey::PageDown), - AutomatedKey::NumpadPageUp => LogicalKey::Named(NamedKey::PageUp), - AutomatedKey::NumpadRight => LogicalKey::Named(NamedKey::ArrowRight), - AutomatedKey::NumpadUp => LogicalKey::Named(NamedKey::ArrowUp), - AutomatedKey::PageDown => LogicalKey::Named(NamedKey::PageDown), - AutomatedKey::PageUp => LogicalKey::Named(NamedKey::PageUp), - AutomatedKey::Pause => LogicalKey::Named(NamedKey::Pause), - AutomatedKey::RightControl => LogicalKey::Named(NamedKey::Control), - AutomatedKey::RightShift => LogicalKey::Named(NamedKey::Shift), - AutomatedKey::ScrollLock => LogicalKey::Named(NamedKey::ScrollLock), - AutomatedKey::Space => LogicalKey::Character(' '), - AutomatedKey::Tab => LogicalKey::Named(NamedKey::Tab), - AutomatedKey::Unknown => LogicalKey::Unknown, + let (logical_key, physical_key) = match automated_key { + AutomatedKey::Char(ch) | AutomatedKey::Numpad(ch) => ( + LogicalKey::Character(ch), + match ch { + 'a' | 'A' => PhysicalKey::KeyA, + 'b' | 'B' => PhysicalKey::KeyB, + 'c' | 'C' => PhysicalKey::KeyC, + 'd' | 'D' => PhysicalKey::KeyD, + 'e' | 'E' => PhysicalKey::KeyE, + 'f' | 'F' => PhysicalKey::KeyF, + 'g' | 'G' => PhysicalKey::KeyG, + 'h' | 'H' => PhysicalKey::KeyH, + 'i' | 'I' => PhysicalKey::KeyI, + 'j' | 'J' => PhysicalKey::KeyJ, + 'k' | 'K' => PhysicalKey::KeyK, + 'l' | 'L' => PhysicalKey::KeyL, + 'm' | 'M' => PhysicalKey::KeyM, + 'n' | 'N' => PhysicalKey::KeyN, + 'o' | 'O' => PhysicalKey::KeyO, + 'p' | 'P' => PhysicalKey::KeyP, + 'q' | 'Q' => PhysicalKey::KeyQ, + 'r' | 'R' => PhysicalKey::KeyR, + 's' | 'S' => PhysicalKey::KeyS, + 't' | 'T' => PhysicalKey::KeyT, + 'u' | 'U' => PhysicalKey::KeyU, + 'v' | 'V' => PhysicalKey::KeyV, + 'w' | 'W' => PhysicalKey::KeyW, + 'x' | 'X' => PhysicalKey::KeyX, + 'y' | 'Y' => PhysicalKey::KeyY, + 'z' | 'Z' => PhysicalKey::KeyZ, + '0' => PhysicalKey::Digit0, + '1' => PhysicalKey::Digit1, + '2' => PhysicalKey::Digit2, + '3' => PhysicalKey::Digit3, + '4' => PhysicalKey::Digit4, + '5' => PhysicalKey::Digit5, + '6' => PhysicalKey::Digit6, + '7' => PhysicalKey::Digit7, + '8' => PhysicalKey::Digit8, + '9' => PhysicalKey::Digit9, + '!' => PhysicalKey::Digit1, + '@' => PhysicalKey::Digit2, + '#' => PhysicalKey::Digit3, + '$' => PhysicalKey::Digit4, + '%' => PhysicalKey::Digit5, + '^' => PhysicalKey::Digit6, + '&' => PhysicalKey::Digit7, + '*' => PhysicalKey::Digit8, + '(' => PhysicalKey::Digit9, + ')' => PhysicalKey::Digit0, + '-' | '_' => PhysicalKey::Minus, + '=' | '+' => PhysicalKey::Equal, + '[' | '{' => PhysicalKey::BracketLeft, + ']' | '}' => PhysicalKey::BracketRight, + '\\' | '|' => PhysicalKey::Backslash, + ';' | ':' => PhysicalKey::Semicolon, + '\'' | '"' => PhysicalKey::Quote, + ',' | '<' => PhysicalKey::Comma, + '.' | '>' => PhysicalKey::Period, + '/' | '?' => PhysicalKey::Slash, + '`' | '~' => PhysicalKey::Backquote, + ' ' => PhysicalKey::Space, + _ => todo!("Key {ch} is unmapped"), + }, + ), + AutomatedKey::ArrowDown => ( + LogicalKey::Named(NamedKey::ArrowDown), + PhysicalKey::ArrowDown, + ), + AutomatedKey::ArrowLeft => ( + LogicalKey::Named(NamedKey::ArrowLeft), + PhysicalKey::ArrowLeft, + ), + AutomatedKey::ArrowRight => ( + LogicalKey::Named(NamedKey::ArrowRight), + PhysicalKey::ArrowRight, + ), + AutomatedKey::ArrowUp => (LogicalKey::Named(NamedKey::ArrowUp), PhysicalKey::ArrowUp), + AutomatedKey::Backspace => ( + LogicalKey::Named(NamedKey::Backspace), + PhysicalKey::Backspace, + ), + AutomatedKey::CapsLock => (LogicalKey::Named(NamedKey::CapsLock), PhysicalKey::CapsLock), + AutomatedKey::Delete => (LogicalKey::Named(NamedKey::Delete), PhysicalKey::Delete), + AutomatedKey::End => (LogicalKey::Named(NamedKey::End), PhysicalKey::End), + AutomatedKey::Enter => (LogicalKey::Named(NamedKey::Enter), PhysicalKey::Enter), + AutomatedKey::Escape => (LogicalKey::Named(NamedKey::Escape), PhysicalKey::Escape), + AutomatedKey::F1 => (LogicalKey::Named(NamedKey::F1), PhysicalKey::F1), + AutomatedKey::F2 => (LogicalKey::Named(NamedKey::F2), PhysicalKey::F2), + AutomatedKey::F3 => (LogicalKey::Named(NamedKey::F3), PhysicalKey::F3), + AutomatedKey::F4 => (LogicalKey::Named(NamedKey::F4), PhysicalKey::F4), + AutomatedKey::F5 => (LogicalKey::Named(NamedKey::F5), PhysicalKey::F5), + AutomatedKey::F6 => (LogicalKey::Named(NamedKey::F6), PhysicalKey::F6), + AutomatedKey::F7 => (LogicalKey::Named(NamedKey::F7), PhysicalKey::F7), + AutomatedKey::F8 => (LogicalKey::Named(NamedKey::F8), PhysicalKey::F8), + AutomatedKey::F9 => (LogicalKey::Named(NamedKey::F9), PhysicalKey::F9), + AutomatedKey::Home => (LogicalKey::Named(NamedKey::Home), PhysicalKey::Home), + AutomatedKey::Insert => (LogicalKey::Named(NamedKey::Insert), PhysicalKey::Insert), + AutomatedKey::LeftAlt => (LogicalKey::Named(NamedKey::Alt), PhysicalKey::AltLeft), + AutomatedKey::LeftControl => ( + LogicalKey::Named(NamedKey::Control), + PhysicalKey::ControlLeft, + ), + AutomatedKey::LeftShift => (LogicalKey::Named(NamedKey::Shift), PhysicalKey::ShiftLeft), + AutomatedKey::NumLock => (LogicalKey::Named(NamedKey::NumLock), PhysicalKey::NumLock), + AutomatedKey::NumpadDelete => (LogicalKey::Named(NamedKey::Delete), PhysicalKey::Delete), + AutomatedKey::NumpadDown => ( + LogicalKey::Named(NamedKey::ArrowDown), + PhysicalKey::ArrowDown, + ), + AutomatedKey::NumpadEnd => (LogicalKey::Named(NamedKey::End), PhysicalKey::End), + AutomatedKey::NumpadHome => (LogicalKey::Named(NamedKey::Home), PhysicalKey::Home), + AutomatedKey::NumpadInsert => (LogicalKey::Named(NamedKey::Insert), PhysicalKey::Insert), + AutomatedKey::NumpadLeft => ( + LogicalKey::Named(NamedKey::ArrowLeft), + PhysicalKey::ArrowLeft, + ), + AutomatedKey::NumpadPageDown => { + (LogicalKey::Named(NamedKey::PageDown), PhysicalKey::PageDown) + } + AutomatedKey::NumpadPageUp => (LogicalKey::Named(NamedKey::PageUp), PhysicalKey::PageUp), + AutomatedKey::NumpadRight => ( + LogicalKey::Named(NamedKey::ArrowRight), + PhysicalKey::ArrowRight, + ), + AutomatedKey::NumpadUp => (LogicalKey::Named(NamedKey::ArrowUp), PhysicalKey::ArrowUp), + AutomatedKey::PageDown => (LogicalKey::Named(NamedKey::PageDown), PhysicalKey::PageDown), + AutomatedKey::PageUp => (LogicalKey::Named(NamedKey::PageUp), PhysicalKey::PageUp), + AutomatedKey::Pause => (LogicalKey::Named(NamedKey::Pause), PhysicalKey::Pause), + AutomatedKey::RightControl => ( + LogicalKey::Named(NamedKey::Control), + PhysicalKey::ControlRight, + ), + AutomatedKey::RightShift => (LogicalKey::Named(NamedKey::Shift), PhysicalKey::ShiftRight), + AutomatedKey::ScrollLock => ( + LogicalKey::Named(NamedKey::ScrollLock), + PhysicalKey::ScrollLock, + ), + AutomatedKey::Space => (LogicalKey::Character(' '), PhysicalKey::Space), + AutomatedKey::Tab => (LogicalKey::Named(NamedKey::Tab), PhysicalKey::Tab), + AutomatedKey::Unknown => (LogicalKey::Unknown, PhysicalKey::Unknown), }; + let key_location = match automated_key { AutomatedKey::Numpad(_) => KeyLocation::Numpad, AutomatedKey::LeftAlt => KeyLocation::Left, @@ -184,9 +280,9 @@ pub fn automated_key_to_descriptor(automated_key: AutomatedKey) -> KeyDescriptor AutomatedKey::RightShift => KeyLocation::Right, _ => KeyLocation::Standard, }; + KeyDescriptor { - // We don't use physical keys in tests - physical_key: PhysicalKey::Unknown, + physical_key, logical_key, key_location, } diff --git a/tests/tests/swfs/avm2/spurious_key_events/Test.as b/tests/tests/swfs/avm2/spurious_key_events/Test.as new file mode 100644 index 000000000000..93c2e729ed95 --- /dev/null +++ b/tests/tests/swfs/avm2/spurious_key_events/Test.as @@ -0,0 +1,25 @@ +package { +import flash.display.*; +import flash.text.*; +import flash.events.*; + +public class Test extends MovieClip { + public function Test() { + var tf = new TextField(); + tf.type = "input"; + addChild(tf); + + tf.addEventListener(TextEvent.TEXT_INPUT, function(evt:TextEvent):void{ + trace("textInput " + evt.text); + }); + tf.addEventListener(KeyboardEvent.KEY_DOWN, function(evt:KeyboardEvent):void{ + trace("keyDown " + evt.keyCode + " " + evt.charCode); + }); + tf.addEventListener(KeyboardEvent.KEY_UP, function(evt:KeyboardEvent):void{ + trace("keyUp " + evt.keyCode + " " + evt.charCode); + }); + + stage.focus = tf; + } +} +} diff --git a/tests/tests/swfs/avm2/spurious_key_events/input.json b/tests/tests/swfs/avm2/spurious_key_events/input.json new file mode 100644 index 000000000000..37dd98f730e7 --- /dev/null +++ b/tests/tests/swfs/avm2/spurious_key_events/input.json @@ -0,0 +1,21 @@ +[ + { "type": "FocusGained" }, + { "type": "Wait" }, + + { "type": "KeyDown", "key": { "Char": "w" } }, + { "type": "KeyUp", "key": { "Char": "w" } }, + + { "type": "KeyUp", "key": { "Char": "a" } }, + + { "type": "KeyDown", "key": { "Char": "w" } }, + { "type": "KeyDown", "key": "LeftShift" }, + { "type": "KeyUp", "key": { "Char": "W" } }, + { "type": "KeyUp", "key": "LeftShift" }, + + { "type": "KeyUp", "key": { "Char": "x" } }, + + { "type": "KeyDown", "key": { "Char": "b" } }, + { "type": "KeyUp", "key": { "Char": "b" } }, + + { "type": "FocusLost" } +] diff --git a/tests/tests/swfs/avm2/spurious_key_events/output.txt b/tests/tests/swfs/avm2/spurious_key_events/output.txt new file mode 100644 index 000000000000..0d47aced48f7 --- /dev/null +++ b/tests/tests/swfs/avm2/spurious_key_events/output.txt @@ -0,0 +1,8 @@ +keyDown 87 119 +keyUp 87 119 +keyDown 87 119 +keyDown 16 0 +keyUp 87 87 +keyUp 16 0 +keyDown 66 98 +keyUp 66 98 diff --git a/tests/tests/swfs/avm2/spurious_key_events/test.swf b/tests/tests/swfs/avm2/spurious_key_events/test.swf new file mode 100644 index 000000000000..24518c99cda6 Binary files /dev/null and b/tests/tests/swfs/avm2/spurious_key_events/test.swf differ diff --git a/tests/tests/swfs/avm2/spurious_key_events/test.toml b/tests/tests/swfs/avm2/spurious_key_events/test.toml new file mode 100644 index 000000000000..568cadba535f --- /dev/null +++ b/tests/tests/swfs/avm2/spurious_key_events/test.toml @@ -0,0 +1 @@ +num_ticks = 2