Skip to main content

lamco_rdp_input/
touch.rs

1//! Touch Event Handling
2//!
3//! Tracks per-contact touch state (MS-RDPEI semantics: down/update/up,
4//! in-range, in-contact, canceled) and turns each wire contact update into
5//! at most one host-facing touch event, with coordinate transformation.
6
7use crate::coordinates::CoordinateTransformer;
8use crate::error::Result;
9use tracing::{debug, warn};
10
11/// Maximum simultaneous contacts. MS-RDPEI's `contactId` is a wire `u8`
12/// (0-255), so this is a hard protocol ceiling, not a tuning choice.
13const MAX_CONTACTS: usize = 256;
14
15/// A touch event ready for host injection.
16#[derive(Debug, Clone, Copy, PartialEq)]
17pub enum TouchEvent {
18    /// A new contact touched down at a stream position.
19    Down {
20        /// Contact slot (same as the wire `contactId`, widened).
21        slot: u32,
22        /// Stream X coordinate.
23        x: f64,
24        /// Stream Y coordinate.
25        y: f64,
26    },
27    /// An engaged contact moved to a stream position.
28    Motion {
29        /// Contact slot.
30        slot: u32,
31        /// Stream X coordinate.
32        x: f64,
33        /// Stream Y coordinate.
34        y: f64,
35    },
36    /// A contact lifted off.
37    Up {
38        /// Contact slot.
39        slot: u32,
40    },
41}
42
43/// The state a single contact is in, mirroring MS-RDPEI's own model
44/// (§ 3.1.1.1): a contact starts out of range, may hover in range without
45/// touching, then engages on contact, and may return to hovering after
46/// lifting (the digitizer can keep tracking a finger just above the
47/// surface) before finally leaving range.
48#[derive(Debug, Clone, Copy, PartialEq, Eq)]
49enum ContactPhase {
50    OutOfRange,
51    Hovering,
52    Engaged,
53}
54
55#[derive(Debug, Clone, Copy)]
56struct ContactState {
57    phase: ContactPhase,
58    /// Set when this contact's position couldn't be mapped to any stream
59    /// (e.g. outside all configured monitors). The state machine keeps
60    /// running normally so `contact_id` bookkeeping stays correct; only the
61    /// host-facing event is suppressed while this is set.
62    ignore: bool,
63}
64
65impl Default for ContactState {
66    fn default() -> Self {
67        Self {
68            phase: ContactPhase::OutOfRange,
69            ignore: false,
70        }
71    }
72}
73
74/// The MS-RDPEI contact flags relevant to a single contact update,
75/// decoded from the wire `contactFlags` bit field (MS-RDPEI § 2.2.3.3.1.1).
76/// Kept as plain booleans rather than depending on an IronRDP crate type,
77/// matching how [`crate::mouse::MouseButton::from_rdp_button`] takes raw
78/// wire values rather than a foreign PDU type.
79#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
80pub struct TouchContactFlags {
81    pub down: bool,
82    pub update: bool,
83    pub up: bool,
84    pub in_range: bool,
85    pub in_contact: bool,
86    pub canceled: bool,
87}
88
89/// Tracks touch contact state and turns wire contact updates into host
90/// touch events.
91pub struct TouchHandler {
92    contacts: Box<[ContactState; MAX_CONTACTS]>,
93}
94
95impl Default for TouchHandler {
96    fn default() -> Self {
97        Self::new()
98    }
99}
100
101impl TouchHandler {
102    pub fn new() -> Self {
103        Self {
104            contacts: Box::new([ContactState::default(); MAX_CONTACTS]),
105        }
106    }
107
108    /// Process one contact update from an MS-RDPEI touch frame.
109    ///
110    /// Returns `Ok(None)` for updates that don't produce a host event
111    /// (hover-only motion, an illegal flag combination, or a position that
112    /// couldn't be mapped to any stream) — none of these are propagated as
113    /// errors, since a single bad contact in a multi-touch frame must not
114    /// abort processing the rest of the frame's contacts.
115    pub fn handle_contact(
116        &mut self,
117        contact_id: u8,
118        x: i32,
119        y: i32,
120        flags: TouchContactFlags,
121        transformer: &mut CoordinateTransformer,
122    ) -> Result<Option<TouchEvent>> {
123        let slot = u32::from(contact_id);
124        let state = &mut self.contacts[contact_id as usize];
125
126        match (flags.down, flags.update, flags.up, flags.in_range, flags.in_contact) {
127            // DOWN|INRANGE|INCONTACT: new contact engaging.
128            (true, false, false, true, true) => {
129                state.phase = ContactPhase::Engaged;
130                state.ignore = false;
131                Self::transform(state, transformer, x, y).map(|pos| pos.map(|(x, y)| TouchEvent::Down { slot, x, y }))
132            }
133
134            // UPDATE|INRANGE|INCONTACT: engaged contact moved.
135            (false, true, false, true, true) => {
136                if state.phase != ContactPhase::Engaged {
137                    warn!(
138                        contact_id,
139                        "touch UPDATE|INCONTACT for a non-engaged contact, treating as down"
140                    );
141                    state.phase = ContactPhase::Engaged;
142                }
143                Self::transform(state, transformer, x, y).map(|pos| pos.map(|(x, y)| TouchEvent::Motion { slot, x, y }))
144            }
145
146            // UPDATE|INRANGE (not in contact): hovering, no host event —
147            // ei::Touchscreen has no hover primitive.
148            (false, true, false, true, false) => {
149                if state.phase == ContactPhase::OutOfRange {
150                    state.phase = ContactPhase::Hovering;
151                }
152                Ok(None)
153            }
154
155            // UP|INRANGE: lifted but still hovering (digitizer keeps
156            // tracking just above the surface) — emit Up, demote rather
157            // than fully release.
158            (false, false, true, true, false) => {
159                let was_engaged = state.phase == ContactPhase::Engaged;
160                state.phase = ContactPhase::Hovering;
161                Ok(was_engaged.then_some(TouchEvent::Up { slot }))
162            }
163
164            // UP, or UP|CANCELED: fully released.
165            (false, false, true, false, false) => {
166                let was_engaged = state.phase == ContactPhase::Engaged;
167                *state = ContactState::default();
168                Ok(was_engaged.then_some(TouchEvent::Up { slot }))
169            }
170
171            _ => {
172                warn!(
173                    contact_id,
174                    ?flags,
175                    "illegal MS-RDPEI touch contact flag combination, ignoring"
176                );
177                Ok(None)
178            }
179        }
180    }
181
182    /// Reset all contact state (e.g. on client reconnection).
183    pub fn reset(&mut self) {
184        *self.contacts = [ContactState::default(); MAX_CONTACTS];
185    }
186
187    fn transform(
188        state: &mut ContactState,
189        transformer: &mut CoordinateTransformer,
190        x: i32,
191        y: i32,
192    ) -> Result<Option<(f64, f64)>> {
193        match transformer.rdp_to_stream(x, y) {
194            Ok((stream_x, stream_y)) => {
195                state.ignore = false;
196                let (stream_x, stream_y) = transformer.clamp_to_bounds(stream_x, stream_y);
197                Ok(Some((stream_x, stream_y)))
198            }
199            Err(e) => {
200                debug!(x, y, error = %e, "touch contact position outside all monitors, suppressing host event");
201                state.ignore = true;
202                Ok(None)
203            }
204        }
205    }
206}
207
208#[cfg(test)]
209mod tests {
210    use super::*;
211    use crate::coordinates::MonitorInfo;
212
213    fn create_test_transformer() -> CoordinateTransformer {
214        let monitor = MonitorInfo {
215            id: 1,
216            name: "Primary".to_string(),
217            x: 0,
218            y: 0,
219            width: 1920,
220            height: 1080,
221            dpi: 96.0,
222            scale_factor: 1.0,
223            stream_x: 0,
224            stream_y: 0,
225            stream_width: 1920,
226            stream_height: 1080,
227            is_primary: true,
228        };
229        CoordinateTransformer::new(vec![monitor]).unwrap()
230    }
231
232    fn down_flags() -> TouchContactFlags {
233        TouchContactFlags {
234            down: true,
235            in_range: true,
236            in_contact: true,
237            ..Default::default()
238        }
239    }
240
241    fn update_flags() -> TouchContactFlags {
242        TouchContactFlags {
243            update: true,
244            in_range: true,
245            in_contact: true,
246            ..Default::default()
247        }
248    }
249
250    fn up_flags() -> TouchContactFlags {
251        TouchContactFlags {
252            up: true,
253            ..Default::default()
254        }
255    }
256
257    #[test]
258    fn test_down_motion_up_sequence() {
259        let mut handler = TouchHandler::new();
260        let mut transformer = create_test_transformer();
261
262        let event = handler
263            .handle_contact(0, 960, 540, down_flags(), &mut transformer)
264            .unwrap();
265        assert!(matches!(event, Some(TouchEvent::Down { slot: 0, .. })));
266
267        let event = handler
268            .handle_contact(0, 970, 540, update_flags(), &mut transformer)
269            .unwrap();
270        assert!(matches!(event, Some(TouchEvent::Motion { slot: 0, .. })));
271
272        let event = handler
273            .handle_contact(0, 970, 540, up_flags(), &mut transformer)
274            .unwrap();
275        assert_eq!(event, Some(TouchEvent::Up { slot: 0 }));
276    }
277
278    #[test]
279    fn test_multiple_contacts_independent_slots() {
280        let mut handler = TouchHandler::new();
281        let mut transformer = create_test_transformer();
282
283        let a = handler
284            .handle_contact(0, 100, 100, down_flags(), &mut transformer)
285            .unwrap();
286        let b = handler
287            .handle_contact(1, 200, 200, down_flags(), &mut transformer)
288            .unwrap();
289
290        assert!(matches!(a, Some(TouchEvent::Down { slot: 0, .. })));
291        assert!(matches!(b, Some(TouchEvent::Down { slot: 1, .. })));
292    }
293
294    #[test]
295    fn test_up_with_inrange_demotes_to_hovering_not_full_release() {
296        let mut handler = TouchHandler::new();
297        let mut transformer = create_test_transformer();
298
299        handler
300            .handle_contact(0, 100, 100, down_flags(), &mut transformer)
301            .unwrap();
302
303        let hover_up = TouchContactFlags {
304            up: true,
305            in_range: true,
306            ..Default::default()
307        };
308        let event = handler.handle_contact(0, 100, 100, hover_up, &mut transformer).unwrap();
309        assert_eq!(event, Some(TouchEvent::Up { slot: 0 }));
310
311        // A second UP for the same (now-hovering) contact must not emit
312        // another Up — it was never re-engaged.
313        let event = handler
314            .handle_contact(0, 100, 100, up_flags(), &mut transformer)
315            .unwrap();
316        assert_eq!(event, None);
317    }
318
319    #[test]
320    fn test_hover_only_produces_no_event() {
321        let mut handler = TouchHandler::new();
322        let mut transformer = create_test_transformer();
323
324        let hover = TouchContactFlags {
325            update: true,
326            in_range: true,
327            ..Default::default()
328        };
329        let event = handler.handle_contact(0, 100, 100, hover, &mut transformer).unwrap();
330        assert_eq!(event, None);
331    }
332
333    #[test]
334    fn test_illegal_flag_combination_is_ignored_not_erroring() {
335        let mut handler = TouchHandler::new();
336        let mut transformer = create_test_transformer();
337
338        // DOWN without INRANGE/INCONTACT is not one of the 8 legal
339        // combinations per MS-RDPEI 2.2.3.3.1.1.
340        let illegal = TouchContactFlags {
341            down: true,
342            ..Default::default()
343        };
344        let event = handler.handle_contact(0, 100, 100, illegal, &mut transformer).unwrap();
345        assert_eq!(event, None);
346    }
347
348    #[test]
349    fn test_out_of_bounds_position_clamps_rather_than_erroring() {
350        let mut handler = TouchHandler::new();
351        let mut transformer = create_test_transformer();
352
353        // Far outside the single 1920x1080 monitor configured above.
354        // CoordinateTransformer falls back to the primary monitor and
355        // clamps rather than failing (matching mouse's own behavior), so
356        // this still produces a Down event, just clamped to the edge.
357        let event = handler
358            .handle_contact(0, 50_000, 50_000, down_flags(), &mut transformer)
359            .unwrap();
360        match event {
361            Some(TouchEvent::Down { x, y, .. }) => {
362                assert!(x <= 1920.0);
363                assert!(y <= 1080.0);
364            }
365            other => panic!("expected a clamped Down event, got {other:?}"),
366        }
367    }
368
369    #[test]
370    fn test_reset_clears_all_contacts() {
371        let mut handler = TouchHandler::new();
372        let mut transformer = create_test_transformer();
373
374        handler
375            .handle_contact(0, 100, 100, down_flags(), &mut transformer)
376            .unwrap();
377        handler.reset();
378
379        // After reset, UP for contact 0 should not report it as
380        // previously-engaged (no Up event since it was never re-downed).
381        let event = handler
382            .handle_contact(0, 100, 100, up_flags(), &mut transformer)
383            .unwrap();
384        assert_eq!(event, None);
385    }
386}