Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 21 additions & 5 deletions core/src/input.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,10 @@ impl ClickEventData {
}

pub struct InputManager {
key_descriptors_down: HashSet<KeyDescriptor>,
/// 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<KeyCode>,
keys_toggled: HashSet<KeyCode>,
Expand All @@ -89,7 +92,7 @@ pub struct InputManager {
impl InputManager {
pub fn new(gamepad_button_mapping: HashMap<GamepadButton, KeyCode>) -> 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,
Expand Down Expand Up @@ -156,22 +159,34 @@ 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,
key_location,
}
}
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
Expand All @@ -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,
Expand Down
192 changes: 144 additions & 48 deletions tests/framework/src/runner/automation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
}
Expand Down
25 changes: 25 additions & 0 deletions tests/tests/swfs/avm2/spurious_key_events/Test.as
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
21 changes: 21 additions & 0 deletions tests/tests/swfs/avm2/spurious_key_events/input.json
Original file line number Diff line number Diff line change
@@ -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" }
]
8 changes: 8 additions & 0 deletions tests/tests/swfs/avm2/spurious_key_events/output.txt
Original file line number Diff line number Diff line change
@@ -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
Binary file not shown.
1 change: 1 addition & 0 deletions tests/tests/swfs/avm2/spurious_key_events/test.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
num_ticks = 2
Loading