keyboard_types/
webdriver.rs

1//! Keyboard related WebDriver functionality.
2//!
3//! The low-level [`KeyInputState::dispatch_keydown`] and
4//! [`KeyInputState::dispatch_keyup`] API creates keyboard events
5//! from WebDriver codes. It is used in the *Perform Actions* API.
6//!
7//! ```rust
8//! # extern crate keyboard_types;
9//! # use keyboard_types::*;
10//! # use keyboard_types::webdriver::*;
11//! let mut state = KeyInputState::new();
12//! let mut keyboard_event = state.dispatch_keydown('a');
13//! assert_eq!(keyboard_event.state, KeyState::Down);
14//! assert_eq!(keyboard_event.key, Key::Character("a".to_owned()));
15//! assert_eq!(keyboard_event.code, Code::KeyA);
16//!
17//! // The `\u{E029}` code is the WebDriver id for the Numpad divide key.
18//! keyboard_event = state.dispatch_keydown('\u{E050}');
19//! assert_eq!(keyboard_event.key, Key::Named(NamedKey::Shift));
20//! assert_eq!(keyboard_event.code, Code::ShiftRight);
21//! assert_eq!(keyboard_event.location, Location::Right);
22//!
23//! keyboard_event = state.dispatch_keyup('\u{E050}').expect("key is released");
24//! keyboard_event = state.dispatch_keyup('a').expect("key is released");
25//! ```
26//!
27//! The higher level [`send_keys`] function is used for the *Element Send Keys*
28//! WebDriver API. It accepts a string and returns a sequence of [`KeyboardEvent`]
29//! and [`CompositionEvent`] values.
30//!
31//! ```rust
32//! # extern crate keyboard_types;
33//! # use keyboard_types::*;
34//! # use keyboard_types::webdriver::*;
35//! let events = send_keys("Hello world!\u{E006}");
36//! println!("{:#?}", events);
37//!
38//! let events = send_keys("A\u{0308}");
39//! println!("{:#?}", events);
40//! ```
41//!
42//! Specification: <https://w3c.github.io/webdriver/>
43
44use alloc::borrow::ToOwned;
45use alloc::string::{String, ToString};
46use alloc::vec::Vec;
47use std::collections::HashSet;
48
49use unicode_segmentation::UnicodeSegmentation;
50
51use crate::{first_char, NamedKey};
52use crate::{Code, Key, KeyState, KeyboardEvent, Location, Modifiers};
53use crate::{CompositionEvent, CompositionState};
54
55// Spec: <https://w3c.github.io/webdriver/#keyboard-actions>
56// normalised (sic) as in british spelling
57fn normalised_key_value(raw_key: char) -> Key {
58    match raw_key {
59        '\u{E000}' => Key::Named(NamedKey::Unidentified),
60        '\u{E001}' => Key::Named(NamedKey::Cancel),
61        '\u{E002}' => Key::Named(NamedKey::Help),
62        '\u{E003}' => Key::Named(NamedKey::Backspace),
63        '\u{E004}' => Key::Named(NamedKey::Tab),
64        '\u{E005}' => Key::Named(NamedKey::Clear),
65        // FIXME: spec says "Return"
66        '\u{E006}' => Key::Named(NamedKey::Enter),
67        '\u{E007}' => Key::Named(NamedKey::Enter),
68        '\u{E008}' => Key::Named(NamedKey::Shift),
69        '\u{E009}' => Key::Named(NamedKey::Control),
70        '\u{E00A}' => Key::Named(NamedKey::Alt),
71        '\u{E00B}' => Key::Named(NamedKey::Pause),
72        '\u{E00C}' => Key::Named(NamedKey::Escape),
73        '\u{E00D}' => Key::Character(" ".to_string()),
74        '\u{E00E}' => Key::Named(NamedKey::PageUp),
75        '\u{E00F}' => Key::Named(NamedKey::PageDown),
76        '\u{E010}' => Key::Named(NamedKey::End),
77        '\u{E011}' => Key::Named(NamedKey::Home),
78        '\u{E012}' => Key::Named(NamedKey::ArrowLeft),
79        '\u{E013}' => Key::Named(NamedKey::ArrowUp),
80        '\u{E014}' => Key::Named(NamedKey::ArrowRight),
81        '\u{E015}' => Key::Named(NamedKey::ArrowDown),
82        '\u{E016}' => Key::Named(NamedKey::Insert),
83        '\u{E017}' => Key::Named(NamedKey::Delete),
84        '\u{E018}' => Key::Character(";".to_string()),
85        '\u{E019}' => Key::Character("=".to_string()),
86        '\u{E01A}' => Key::Character("0".to_string()),
87        '\u{E01B}' => Key::Character("1".to_string()),
88        '\u{E01C}' => Key::Character("2".to_string()),
89        '\u{E01D}' => Key::Character("3".to_string()),
90        '\u{E01E}' => Key::Character("4".to_string()),
91        '\u{E01F}' => Key::Character("5".to_string()),
92        '\u{E020}' => Key::Character("6".to_string()),
93        '\u{E021}' => Key::Character("7".to_string()),
94        '\u{E022}' => Key::Character("8".to_string()),
95        '\u{E023}' => Key::Character("9".to_string()),
96        '\u{E024}' => Key::Character("*".to_string()),
97        '\u{E025}' => Key::Character("+".to_string()),
98        '\u{E026}' => Key::Character(",".to_string()),
99        '\u{E027}' => Key::Character("-".to_string()),
100        '\u{E028}' => Key::Character(".".to_string()),
101        '\u{E029}' => Key::Character("/".to_string()),
102        '\u{E031}' => Key::Named(NamedKey::F1),
103        '\u{E032}' => Key::Named(NamedKey::F2),
104        '\u{E033}' => Key::Named(NamedKey::F3),
105        '\u{E034}' => Key::Named(NamedKey::F4),
106        '\u{E035}' => Key::Named(NamedKey::F5),
107        '\u{E036}' => Key::Named(NamedKey::F6),
108        '\u{E037}' => Key::Named(NamedKey::F7),
109        '\u{E038}' => Key::Named(NamedKey::F8),
110        '\u{E039}' => Key::Named(NamedKey::F9),
111        '\u{E03A}' => Key::Named(NamedKey::F10),
112        '\u{E03B}' => Key::Named(NamedKey::F11),
113        '\u{E03C}' => Key::Named(NamedKey::F12),
114        '\u{E03D}' => Key::Named(NamedKey::Meta),
115        '\u{E040}' => Key::Named(NamedKey::ZenkakuHankaku),
116        '\u{E050}' => Key::Named(NamedKey::Shift),
117        '\u{E051}' => Key::Named(NamedKey::Control),
118        '\u{E052}' => Key::Named(NamedKey::Alt),
119        '\u{E053}' => Key::Named(NamedKey::Meta),
120        '\u{E054}' => Key::Named(NamedKey::PageUp),
121        '\u{E055}' => Key::Named(NamedKey::PageDown),
122        '\u{E056}' => Key::Named(NamedKey::End),
123        '\u{E057}' => Key::Named(NamedKey::Home),
124        '\u{E058}' => Key::Named(NamedKey::ArrowLeft),
125        '\u{E059}' => Key::Named(NamedKey::ArrowUp),
126        '\u{E05A}' => Key::Named(NamedKey::ArrowRight),
127        '\u{E05B}' => Key::Named(NamedKey::ArrowDown),
128        '\u{E05C}' => Key::Named(NamedKey::Insert),
129        '\u{E05D}' => Key::Named(NamedKey::Delete),
130        _ => Key::Character(raw_key.to_string()),
131    }
132}
133
134/// Spec: <https://w3c.github.io/webdriver/#dfn-code>
135fn code(raw_key: char) -> Code {
136    match raw_key {
137        '`' | '~' => Code::Backquote,
138        '\\' | '|' => Code::Backslash,
139        '\u{E003}' => Code::Backspace,
140        '[' | '{' => Code::BracketLeft,
141        ']' | '}' => Code::BracketRight,
142        ',' | '<' => Code::Comma,
143        '0' | ')' => Code::Digit0,
144        '1' | '!' => Code::Digit1,
145        '2' | '@' => Code::Digit2,
146        '3' | '#' => Code::Digit3,
147        '4' | '$' => Code::Digit4,
148        '5' | '%' => Code::Digit5,
149        '6' | '^' => Code::Digit6,
150        '7' | '&' => Code::Digit7,
151        '8' | '*' => Code::Digit8,
152        '9' | '(' => Code::Digit9,
153        '=' | '+' => Code::Equal,
154        // FIXME: spec has '<' | '>' => Code::IntlBackslash,
155        'a' | 'A' => Code::KeyA,
156        'b' | 'B' => Code::KeyB,
157        'c' | 'C' => Code::KeyC,
158        'd' | 'D' => Code::KeyD,
159        'e' | 'E' => Code::KeyE,
160        'f' | 'F' => Code::KeyF,
161        'g' | 'G' => Code::KeyG,
162        'h' | 'H' => Code::KeyH,
163        'i' | 'I' => Code::KeyI,
164        'j' | 'J' => Code::KeyJ,
165        'k' | 'K' => Code::KeyK,
166        'l' | 'L' => Code::KeyL,
167        'm' | 'M' => Code::KeyM,
168        'n' | 'N' => Code::KeyN,
169        'o' | 'O' => Code::KeyO,
170        'p' | 'P' => Code::KeyP,
171        'q' | 'Q' => Code::KeyQ,
172        'r' | 'R' => Code::KeyR,
173        's' | 'S' => Code::KeyS,
174        't' | 'T' => Code::KeyT,
175        'u' | 'U' => Code::KeyU,
176        'v' | 'V' => Code::KeyV,
177        'w' | 'W' => Code::KeyW,
178        'x' | 'X' => Code::KeyX,
179        'y' | 'Y' => Code::KeyY,
180        'z' | 'Z' => Code::KeyZ,
181        '-' | '_' => Code::Minus,
182        '.' | '>' => Code::Period,
183        '\'' | '"' => Code::Quote,
184        ';' | ':' => Code::Semicolon,
185        '/' | '?' => Code::Slash,
186        '\u{E00A}' => Code::AltLeft,
187        '\u{E052}' => Code::AltRight,
188        '\u{E009}' => Code::ControlLeft,
189        '\u{E051}' => Code::ControlRight,
190        '\u{E006}' => Code::Enter,
191        '\u{E00B}' => Code::Pause,
192        // FIXME: spec says "OSLeft"
193        '\u{E03D}' => Code::MetaLeft,
194        // FIXME: spec says "OSRight"
195        '\u{E053}' => Code::MetaRight,
196        '\u{E008}' => Code::ShiftLeft,
197        '\u{E050}' => Code::ShiftRight,
198        ' ' | '\u{E00D}' => Code::Space,
199        '\u{E004}' => Code::Tab,
200        '\u{E017}' => Code::Delete,
201        '\u{E010}' => Code::End,
202        '\u{E002}' => Code::Help,
203        '\u{E011}' => Code::Home,
204        '\u{E016}' => Code::Insert,
205        // FIXME: spec says '\u{E01E}' => Code::PageDown, which is Numpad 4
206        '\u{E00F}' => Code::PageDown,
207        // FIXME: spec says '\u{E01F}' => Code::PageUp, which is Numpad 5
208        '\u{E00E}' => Code::PageUp,
209        '\u{E015}' => Code::ArrowDown,
210        '\u{E012}' => Code::ArrowLeft,
211        '\u{E014}' => Code::ArrowRight,
212        '\u{E013}' => Code::ArrowUp,
213        '\u{E00C}' => Code::Escape,
214        '\u{E031}' => Code::F1,
215        '\u{E032}' => Code::F2,
216        '\u{E033}' => Code::F3,
217        '\u{E034}' => Code::F4,
218        '\u{E035}' => Code::F5,
219        '\u{E036}' => Code::F6,
220        '\u{E037}' => Code::F7,
221        '\u{E038}' => Code::F8,
222        '\u{E039}' => Code::F9,
223        '\u{E03A}' => Code::F10,
224        '\u{E03B}' => Code::F11,
225        '\u{E03C}' => Code::F12,
226        '\u{E019}' => Code::NumpadEqual,
227        '\u{E01A}' | '\u{E05C}' => Code::Numpad0,
228        '\u{E01B}' | '\u{E056}' => Code::Numpad1,
229        '\u{E01C}' | '\u{E05B}' => Code::Numpad2,
230        '\u{E01D}' | '\u{E055}' => Code::Numpad3,
231        '\u{E01E}' | '\u{E058}' => Code::Numpad4,
232        '\u{E01F}' => Code::Numpad5,
233        '\u{E020}' | '\u{E05A}' => Code::Numpad6,
234        '\u{E021}' | '\u{E057}' => Code::Numpad7,
235        '\u{E022}' | '\u{E059}' => Code::Numpad8,
236        '\u{E023}' | '\u{E054}' => Code::Numpad9,
237        // FIXME: spec says uE024
238        '\u{E025}' => Code::NumpadAdd,
239        '\u{E026}' => Code::NumpadComma,
240        '\u{E028}' | '\u{E05D}' => Code::NumpadDecimal,
241        '\u{E029}' => Code::NumpadDivide,
242        '\u{E007}' => Code::NumpadEnter,
243        '\u{E024}' => Code::NumpadMultiply,
244        // FIXME: spec says uE026
245        '\u{E027}' => Code::NumpadSubtract,
246        _ => Code::Unidentified,
247    }
248}
249
250fn is_shifted_character(raw_key: char) -> bool {
251    matches!(
252        raw_key,
253        '~' | '|'
254            | '{'
255            | '}'
256            | '<'
257            | ')'
258            | '!'
259            | '@'
260            | '#'
261            | '$'
262            | '%'
263            | '^'
264            | '&'
265            | '*'
266            | '('
267            | '+'
268            | '>'
269            | '_'
270            | '\"'
271            | ':'
272            | '?'
273            | '\u{E00D}'
274            | '\u{E05C}'
275            | '\u{E056}'
276            | '\u{E05B}'
277            | '\u{E055}'
278            | '\u{E058}'
279            | '\u{E05A}'
280            | '\u{E057}'
281            | '\u{E059}'
282            | '\u{E054}'
283            | '\u{E05D}'
284            | 'A'..='Z'
285    )
286}
287
288fn key_location(raw_key: char) -> Location {
289    match raw_key {
290        '\u{E007}'..='\u{E00A}' => Location::Left,
291        '\u{E01A}'..='\u{E029}' => Location::Numpad,
292        '\u{E03D}' => Location::Left,
293        '\u{E050}'..='\u{E053}' => Location::Right,
294        '\u{E054}'..='\u{E05D}' => Location::Numpad,
295        '\u{E019}' => Location::Numpad,
296        _ => Location::Standard,
297    }
298}
299
300fn get_modifier(key: &Key) -> Modifiers {
301    match key {
302        Key::Named(NamedKey::Alt) => Modifiers::ALT,
303        Key::Named(NamedKey::Shift) => Modifiers::SHIFT,
304        Key::Named(NamedKey::Control) => Modifiers::CONTROL,
305        Key::Named(NamedKey::Meta) => Modifiers::META,
306        _ => Modifiers::empty(),
307    }
308}
309
310/// Store pressed keys and modifiers.
311///
312/// Spec: <https://w3c.github.io/webdriver/#dfn-key-input-state>
313#[derive(Clone, Debug, Default)]
314#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
315pub struct KeyInputState {
316    pressed: HashSet<Key>,
317    modifiers: Modifiers,
318}
319
320impl KeyInputState {
321    /// New state without any keys or modifiers pressed.
322    ///
323    /// Same as the default value.
324    pub fn new() -> KeyInputState {
325        KeyInputState::default()
326    }
327
328    /// Get a keyboard-keydown event from a WebDriver key value.
329    ///
330    /// Stores that the key is pressed in the state object.
331    ///
332    /// The input cancel list is not implemented here but can be emulated
333    /// by adding the `raw_key` value with a `keyUp` action to a list
334    /// before executing this function.
335    ///
336    /// Specification: <https://w3c.github.io/webdriver/#dfn-dispatch-a-keydown-action>
337    pub fn dispatch_keydown(&mut self, raw_key: char) -> KeyboardEvent {
338        let key = normalised_key_value(raw_key);
339        let repeat = self.pressed.contains(&key);
340        let code = code(raw_key);
341        let location = key_location(raw_key);
342        self.modifiers.insert(get_modifier(&key));
343        self.pressed.insert(key.clone());
344        KeyboardEvent {
345            state: KeyState::Down,
346            key,
347            code,
348            location,
349            modifiers: self.modifiers,
350            repeat,
351            is_composing: false,
352        }
353    }
354
355    /// Get a keyboard-keyup event from a WebDriver key value.
356    ///
357    /// Updates state. Returns `None` if the key is not listed as pressed.
358    ///
359    /// Specification: <https://w3c.github.io/webdriver/#dfn-dispatch-a-keyup-action>
360    pub fn dispatch_keyup(&mut self, raw_key: char) -> Option<KeyboardEvent> {
361        let key = normalised_key_value(raw_key);
362        if !self.pressed.contains(&key) {
363            return None;
364        }
365        let code = code(raw_key);
366        let location = key_location(raw_key);
367        self.modifiers.remove(get_modifier(&key));
368        self.pressed.remove(&key);
369        Some(KeyboardEvent {
370            state: KeyState::Up,
371            key,
372            code,
373            location,
374            modifiers: self.modifiers,
375            repeat: false,
376            is_composing: false,
377        })
378    }
379
380    fn clear(&mut self, undo_actions: &mut HashSet<char>, result: &mut Vec<Event>) {
381        let mut actions: Vec<_> = undo_actions.drain().collect();
382        actions.sort_unstable();
383        for action in actions {
384            result.push(self.dispatch_keyup(action).unwrap().into());
385        }
386        assert!(undo_actions.is_empty());
387    }
388
389    fn dispatch_typeable(&mut self, text: &mut String, result: &mut Vec<Event>) {
390        for character in text.chars() {
391            let shifted = self.modifiers.contains(Modifiers::SHIFT);
392            if is_shifted_character(character) && !shifted {
393                // dispatch left shift down
394                result.push(self.dispatch_keydown('\u{E008}').into());
395            }
396            if !is_shifted_character(character) && shifted {
397                // dispatch left shift up
398                result.push(self.dispatch_keyup('\u{E008}').unwrap().into());
399            }
400            result.push(self.dispatch_keydown(character).into());
401            result.push(self.dispatch_keyup(character).unwrap().into());
402        }
403        text.clear();
404    }
405}
406
407/// Either a [`KeyboardEvent`] or a [`CompositionEvent`].
408///
409/// Returned by the [`send_keys`] function.
410#[derive(Clone, Eq, PartialEq, Hash, Debug, PartialOrd, Ord)]
411#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
412pub enum Event {
413    Keyboard(KeyboardEvent),
414    Composition(CompositionEvent),
415}
416
417impl From<KeyboardEvent> for Event {
418    fn from(v: KeyboardEvent) -> Event {
419        Event::Keyboard(v)
420    }
421}
422
423impl From<CompositionEvent> for Event {
424    fn from(v: CompositionEvent) -> Event {
425        Event::Composition(v)
426    }
427}
428
429/// Compute the events resulting from a WebDriver *Element Send Keys* command.
430///
431/// Spec: <https://w3c.github.io/webdriver/#element-send-keys>
432pub fn send_keys(text: &str) -> Vec<Event> {
433    #[allow(deprecated)]
434    fn is_modifier(text: &str) -> bool {
435        if text.chars().count() != 1 {
436            return false;
437        }
438        // values from <https://www.w3.org/TR/uievents-key/#keys-modifier>
439        matches!(
440            normalised_key_value(first_char(text)),
441            Key::Named(
442                NamedKey::Alt
443                    | NamedKey::AltGraph
444                    | NamedKey::CapsLock
445                    | NamedKey::Control
446                    | NamedKey::Fn
447                    | NamedKey::FnLock
448                    | NamedKey::Meta
449                    | NamedKey::NumLock
450                    | NamedKey::ScrollLock
451                    | NamedKey::Shift
452                    | NamedKey::Symbol
453                    | NamedKey::SymbolLock
454                    | NamedKey::Hyper
455                    | NamedKey::Super
456            )
457        )
458    }
459
460    /// Spec: <https://w3c.github.io/webdriver/#dfn-typeable>
461    fn is_typeable(text: &str) -> bool {
462        text.chars().count() == 1
463    }
464
465    let mut result = Vec::new();
466    let mut typeable_text = String::new();
467    let mut state = KeyInputState::new();
468    let mut undo_actions = HashSet::new();
469    for cluster in UnicodeSegmentation::graphemes(text, true) {
470        match cluster {
471            "\u{E000}" => {
472                state.dispatch_typeable(&mut typeable_text, &mut result);
473                state.clear(&mut undo_actions, &mut result);
474            }
475            s if is_modifier(s) => {
476                state.dispatch_typeable(&mut typeable_text, &mut result);
477                let raw_modifier = first_char(s);
478                result.push(state.dispatch_keydown(raw_modifier).into());
479                undo_actions.insert(raw_modifier);
480            }
481            s if is_typeable(s) => typeable_text.push_str(s),
482            s => {
483                state.dispatch_typeable(&mut typeable_text, &mut result);
484                // FIXME: Spec says undefined instead of empty string
485                result.push(
486                    CompositionEvent {
487                        state: CompositionState::Start,
488                        data: String::new(),
489                    }
490                    .into(),
491                );
492                result.push(
493                    CompositionEvent {
494                        state: CompositionState::Update,
495                        data: s.to_owned(),
496                    }
497                    .into(),
498                );
499                result.push(
500                    CompositionEvent {
501                        state: CompositionState::End,
502                        data: s.to_owned(),
503                    }
504                    .into(),
505                );
506            }
507        }
508    }
509    state.dispatch_typeable(&mut typeable_text, &mut result);
510    state.clear(&mut undo_actions, &mut result);
511    result
512}