ironrdp_input/
lib.rs

1#![cfg_attr(doc, doc = include_str!("../README.md"))]
2#![doc(html_logo_url = "https://cdnweb.devolutions.net/images/projects/devolutions/logos/devolutions-icon-shadow.svg")]
3
4use std::collections::BTreeSet;
5
6use bitvec::array::BitArray;
7use bitvec::BitArr;
8use ironrdp_pdu::input::fast_path::{FastPathInputEvent, KeyboardFlags};
9use ironrdp_pdu::input::mouse::PointerFlags;
10use ironrdp_pdu::input::mouse_x::PointerXFlags;
11use ironrdp_pdu::input::{MousePdu, MouseXPdu};
12use smallvec::SmallVec;
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
15#[repr(u8)]
16pub enum MouseButton {
17    Left = 0,
18    Middle = 1,
19    Right = 2,
20    /// Typically Browser Back button
21    X1 = 3,
22    /// Typically Browser Forward button
23    X2 = 4,
24}
25
26impl MouseButton {
27    #[expect(
28        clippy::as_conversions,
29        reason = "guarantees discriminant layout, and as is the only way to cast enum -> primitive"
30    )]
31    pub fn as_idx(self) -> usize {
32        self as usize
33    }
34
35    pub fn from_idx(idx: usize) -> Option<Self> {
36        match idx {
37            0 => Some(Self::Left),
38            1 => Some(Self::Middle),
39            2 => Some(Self::Right),
40            3 => Some(Self::X1),
41            4 => Some(Self::X2),
42            _ => None,
43        }
44    }
45
46    pub fn from_web_button(value: u8) -> Option<Self> {
47        // https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/button#value
48        match value {
49            0 => Some(Self::Left),
50            1 => Some(Self::Middle),
51            2 => Some(Self::Right),
52            3 => Some(Self::X1),
53            4 => Some(Self::X2),
54            _ => None,
55        }
56    }
57
58    pub fn from_native_button(value: u16) -> Option<Self> {
59        match value {
60            1 => Some(Self::Left),
61            2 => Some(Self::Middle),
62            3 => Some(Self::Right),
63            8 => Some(Self::X1),
64            9 => Some(Self::X2),
65            _ => None,
66        }
67    }
68}
69
70/// Keyboard scan code.
71#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
72pub struct Scancode {
73    code: u8,
74    extended: bool,
75}
76
77impl Scancode {
78    pub const fn from_u8(extended: bool, code: u8) -> Self {
79        Self { code, extended }
80    }
81
82    pub const fn from_u16(scancode: u16) -> Self {
83        let extended = scancode & 0xE000 == 0xE000;
84
85        #[expect(
86            clippy::as_conversions,
87            clippy::cast_possible_truncation,
88            reason = "truncating on purpose"
89        )]
90        let code = scancode as u8;
91
92        Self { code, extended }
93    }
94
95    pub fn as_idx(self) -> usize {
96        if self.extended {
97            #[expect(clippy::missing_panics_doc, reason = "unreachable panic (integer upcast)")]
98            usize::from(self.code).checked_add(256).expect("never overflow")
99        } else {
100            usize::from(self.code)
101        }
102    }
103
104    pub fn as_u8(self) -> (bool, u8) {
105        (self.extended, self.code)
106    }
107
108    pub fn as_u16(self) -> u16 {
109        if self.extended {
110            u16::from(self.code) | 0xE000
111        } else {
112            u16::from(self.code)
113        }
114    }
115}
116
117impl From<(bool, u8)> for Scancode {
118    fn from((extended, code): (bool, u8)) -> Self {
119        Self::from_u8(extended, code)
120    }
121}
122
123impl From<u16> for Scancode {
124    fn from(code: u16) -> Self {
125        Self::from_u16(code)
126    }
127}
128
129/// Cursor position for a mouse device.
130#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
131pub struct MousePosition {
132    pub x: u16,
133    pub y: u16,
134}
135
136/// Mouse wheel rotations.
137#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
138pub struct WheelRotations {
139    pub is_vertical: bool,
140    pub rotation_units: i16,
141}
142
143#[derive(Debug, Clone)]
144pub enum Operation {
145    MouseButtonPressed(MouseButton),
146    MouseButtonReleased(MouseButton),
147    MouseMove(MousePosition),
148    WheelRotations(WheelRotations),
149    KeyPressed(Scancode),
150    KeyReleased(Scancode),
151    UnicodeKeyPressed(char),
152    UnicodeKeyReleased(char),
153}
154
155pub type KeyboardState = BitArr!(for 512);
156pub type MouseButtonsState = BitArr!(for 5);
157
158/// In-memory database for maintaining the current keyboard and mouse state.
159pub struct Database {
160    unicode_keyboard_state: BTreeSet<char>,
161    keyboard: KeyboardState,
162    mouse_buttons: MouseButtonsState,
163    mouse_position: MousePosition,
164}
165
166impl Default for Database {
167    fn default() -> Self {
168        Self::new()
169    }
170}
171
172impl Database {
173    pub fn new() -> Self {
174        Self {
175            keyboard: BitArray::ZERO,
176            mouse_buttons: BitArray::ZERO,
177            mouse_position: MousePosition { x: 0, y: 0 },
178            unicode_keyboard_state: BTreeSet::new(),
179        }
180    }
181
182    pub fn is_unicode_key_pressed(&self, character: char) -> bool {
183        self.unicode_keyboard_state.contains(&character)
184    }
185
186    pub fn is_key_pressed(&self, scancode: Scancode) -> bool {
187        self.keyboard
188            .get(scancode.as_idx())
189            .as_deref()
190            .copied()
191            .unwrap_or(false)
192    }
193
194    pub fn is_mouse_button_pressed(&self, button: MouseButton) -> bool {
195        self.mouse_buttons
196            .get(button.as_idx())
197            .as_deref()
198            .copied()
199            .unwrap_or(false)
200    }
201
202    pub fn mouse_position(&self) -> MousePosition {
203        self.mouse_position
204    }
205
206    pub fn keyboard_state(&self) -> &KeyboardState {
207        &self.keyboard
208    }
209
210    pub fn mouse_buttons_state(&self) -> &MouseButtonsState {
211        &self.mouse_buttons
212    }
213
214    /// Apply a transaction (list of operations) and returns a list of RDP input events to send.
215    ///
216    /// Operations that would cause no state change are ignored.
217    pub fn apply(&mut self, transaction: impl IntoIterator<Item = Operation>) -> SmallVec<[FastPathInputEvent; 2]> {
218        let mut events = SmallVec::new();
219
220        for operation in transaction {
221            match operation {
222                Operation::MouseButtonPressed(button) => {
223                    let was_pressed = self.mouse_buttons.replace(button.as_idx(), true);
224
225                    if !was_pressed {
226                        let event = match MouseButtonFlags::from(button) {
227                            MouseButtonFlags::Button(flags) => FastPathInputEvent::MouseEvent(MousePdu {
228                                flags: PointerFlags::DOWN | flags,
229                                number_of_wheel_rotation_units: 0,
230                                x_position: self.mouse_position.x,
231                                y_position: self.mouse_position.y,
232                            }),
233                            MouseButtonFlags::Pointer(flags) => FastPathInputEvent::MouseEventEx(MouseXPdu {
234                                flags: PointerXFlags::DOWN | flags,
235                                x_position: self.mouse_position.x,
236                                y_position: self.mouse_position.y,
237                            }),
238                        };
239
240                        events.push(event)
241                    }
242                }
243                Operation::MouseButtonReleased(button) => {
244                    let was_pressed = self.mouse_buttons.replace(button.as_idx(), false);
245
246                    if was_pressed {
247                        let event = match MouseButtonFlags::from(button) {
248                            MouseButtonFlags::Button(flags) => FastPathInputEvent::MouseEvent(MousePdu {
249                                flags,
250                                number_of_wheel_rotation_units: 0,
251                                x_position: self.mouse_position.x,
252                                y_position: self.mouse_position.y,
253                            }),
254                            MouseButtonFlags::Pointer(flags) => FastPathInputEvent::MouseEventEx(MouseXPdu {
255                                flags,
256                                x_position: self.mouse_position.x,
257                                y_position: self.mouse_position.y,
258                            }),
259                        };
260
261                        events.push(event)
262                    }
263                }
264                Operation::MouseMove(position) => {
265                    if position != self.mouse_position {
266                        self.mouse_position = position;
267                        events.push(FastPathInputEvent::MouseEvent(MousePdu {
268                            flags: PointerFlags::MOVE,
269                            number_of_wheel_rotation_units: 0,
270                            x_position: position.x,
271                            y_position: position.y,
272                        }))
273                    }
274                }
275                Operation::WheelRotations(rotations) => events.push(FastPathInputEvent::MouseEvent(MousePdu {
276                    flags: if rotations.is_vertical {
277                        PointerFlags::VERTICAL_WHEEL
278                    } else {
279                        PointerFlags::HORIZONTAL_WHEEL
280                    },
281                    number_of_wheel_rotation_units: rotations.rotation_units,
282                    x_position: self.mouse_position.x,
283                    y_position: self.mouse_position.y,
284                })),
285                Operation::KeyPressed(scancode) => {
286                    let was_pressed = self.keyboard.replace(scancode.as_idx(), true);
287
288                    let mut flags = KeyboardFlags::empty();
289
290                    if scancode.extended {
291                        flags |= KeyboardFlags::EXTENDED
292                    };
293
294                    if was_pressed {
295                        events.push(FastPathInputEvent::KeyboardEvent(
296                            flags | KeyboardFlags::RELEASE,
297                            scancode.code,
298                        ));
299                    }
300
301                    events.push(FastPathInputEvent::KeyboardEvent(flags, scancode.code));
302                }
303                Operation::KeyReleased(scancode) => {
304                    let was_pressed = self.keyboard.replace(scancode.as_idx(), false);
305
306                    let mut flags = KeyboardFlags::RELEASE;
307
308                    if scancode.extended {
309                        flags |= KeyboardFlags::EXTENDED
310                    };
311
312                    if was_pressed {
313                        events.push(FastPathInputEvent::KeyboardEvent(flags, scancode.code));
314                    }
315                }
316                Operation::UnicodeKeyPressed(character) => {
317                    let was_pressed = !self.unicode_keyboard_state.insert(character);
318
319                    let mut utf16_buffer = [0u16; 2];
320                    let utf16_code_units = character.encode_utf16(&mut utf16_buffer);
321
322                    if was_pressed {
323                        for code in utf16_code_units.iter() {
324                            events.push(FastPathInputEvent::UnicodeKeyboardEvent(KeyboardFlags::RELEASE, *code));
325                        }
326                    }
327
328                    for code in utf16_code_units {
329                        events.push(FastPathInputEvent::UnicodeKeyboardEvent(KeyboardFlags::empty(), *code));
330                    }
331                }
332                Operation::UnicodeKeyReleased(character) => {
333                    let was_pressed = self.unicode_keyboard_state.remove(&character);
334
335                    let mut utf16_buffer = [0u16; 2];
336                    let utf16_code_units = character.encode_utf16(&mut utf16_buffer);
337
338                    if was_pressed {
339                        for code in utf16_code_units {
340                            events.push(FastPathInputEvent::UnicodeKeyboardEvent(KeyboardFlags::RELEASE, *code));
341                        }
342                    }
343                }
344            }
345        }
346
347        events
348    }
349
350    /// Releases all keys and buttons. Returns a list of RDP input events to send.
351    pub fn release_all(&mut self) -> SmallVec<[FastPathInputEvent; 2]> {
352        let mut events = SmallVec::new();
353
354        for idx in self.mouse_buttons.iter_ones() {
355            #[expect(clippy::missing_panics_doc, reason = "unreachable panic (checked integer downcast)")]
356            let button = MouseButton::from_idx(idx).expect("in-range index");
357
358            let event = match MouseButtonFlags::from(button) {
359                MouseButtonFlags::Button(flags) => FastPathInputEvent::MouseEvent(MousePdu {
360                    flags,
361                    number_of_wheel_rotation_units: 0,
362                    x_position: self.mouse_position.x,
363                    y_position: self.mouse_position.y,
364                }),
365                MouseButtonFlags::Pointer(flags) => FastPathInputEvent::MouseEventEx(MouseXPdu {
366                    flags,
367                    x_position: self.mouse_position.x,
368                    y_position: self.mouse_position.y,
369                }),
370            };
371
372            events.push(event)
373        }
374
375        // The keyboard bit array size is 512.
376        for idx in self.keyboard.iter_ones() {
377            let (scancode, extended) = if idx >= 256 {
378                #[expect(clippy::missing_panics_doc, reason = "unreachable panic (checked integer underflow)")]
379                let extended_code = idx.checked_sub(256).expect("never underflow");
380                #[expect(clippy::missing_panics_doc, reason = "unreachable panic (checked integer downcast)")]
381                (u8::try_from(extended_code).expect("always in the range"), true)
382            } else {
383                #[expect(clippy::missing_panics_doc, reason = "unreachable panic (checked integer downcast)")]
384                (u8::try_from(idx).expect("always in the range"), false)
385            };
386
387            let mut flags = KeyboardFlags::RELEASE;
388
389            if extended {
390                flags |= KeyboardFlags::EXTENDED
391            };
392
393            events.push(FastPathInputEvent::KeyboardEvent(flags, scancode));
394        }
395
396        for character in core::mem::take(&mut self.unicode_keyboard_state).into_iter() {
397            let mut utf16_buffer = [0u16; 2];
398            let utf16_code_units = character.encode_utf16(&mut utf16_buffer);
399
400            for code in utf16_code_units {
401                events.push(FastPathInputEvent::UnicodeKeyboardEvent(KeyboardFlags::RELEASE, *code));
402            }
403        }
404
405        self.mouse_buttons = BitArray::ZERO;
406        self.keyboard = BitArray::ZERO;
407
408        events
409    }
410}
411
412/// Returns the RDP input event to send in order to synchronize lock keys.
413pub fn synchronize_event(scroll_lock: bool, num_lock: bool, caps_lock: bool, kana_lock: bool) -> FastPathInputEvent {
414    use ironrdp_pdu::input::fast_path::SynchronizeFlags;
415
416    let mut flags = SynchronizeFlags::empty();
417
418    if scroll_lock {
419        flags |= SynchronizeFlags::SCROLL_LOCK;
420    }
421
422    if num_lock {
423        flags |= SynchronizeFlags::NUM_LOCK;
424    }
425
426    if caps_lock {
427        flags |= SynchronizeFlags::CAPS_LOCK;
428    }
429
430    if kana_lock {
431        flags |= SynchronizeFlags::KANA_LOCK;
432    }
433
434    FastPathInputEvent::SyncEvent(flags)
435}
436
437enum MouseButtonFlags {
438    Button(PointerFlags),
439    Pointer(PointerXFlags),
440}
441
442impl From<MouseButton> for MouseButtonFlags {
443    fn from(value: MouseButton) -> Self {
444        match value {
445            MouseButton::Left => Self::Button(PointerFlags::LEFT_BUTTON),
446            MouseButton::Middle => Self::Button(PointerFlags::MIDDLE_BUTTON_OR_WHEEL),
447            MouseButton::Right => Self::Button(PointerFlags::RIGHT_BUTTON),
448            MouseButton::X1 => Self::Pointer(PointerXFlags::BUTTON1),
449            MouseButton::X2 => Self::Pointer(PointerXFlags::BUTTON2),
450        }
451    }
452}