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 X1 = 3,
22 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 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#[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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
131pub struct MousePosition {
132 pub x: u16,
133 pub y: u16,
134}
135
136#[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
158pub 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 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 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 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
412pub 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}