rustenium/input/bidi/
touchscreen.rs

1use rustenium_bidi_commands::browsing_context::types::BrowsingContext;
2use rustenium_bidi_commands::input::commands::{
3    InputCommand, PerformActions, PerformActionsParameters, InputPerformActionsMethod,
4};
5use rustenium_bidi_commands::input::types::{
6    PointerSourceActions, PointerSourceAction, PointerDownAction, PointerUpAction,
7    PointerMoveAction, SourceActions, PointerEnum, PointerDownEnum, PointerUpEnum,
8    PointerMoveEnum, PointerCommonProperties, PointerParameters, PointerType, Origin,
9};
10use rustenium_bidi_commands::CommandData;
11use rustenium_core::Session;
12use rustenium_core::transport::ConnectionTransport;
13use crate::error::InputError;
14use std::sync::Arc;
15use tokio::sync::Mutex;
16use crate::input::mouse::Point;
17use super::{FINGER_ID_PREFIX};
18
19/// Options for touch movement operations.
20#[derive(Debug, Clone, Default)]
21pub struct TouchMoveOptions {
22    /// Origin for the touch movement.
23    pub origin: Option<Origin>,
24}
25
26/// Handle representing a single touch point for multi-touch gestures.
27///
28/// Each `TouchHandle` represents one finger/touch point. Create multiple handles
29/// to simulate multi-touch gestures like pinch, zoom, or multi-finger swipes.
30pub struct TouchHandle<OT: ConnectionTransport> {
31    session: Arc<Mutex<Session<OT>>>,
32    touchscreen: Arc<Touchscreen<OT>>,
33    id: usize,
34    bidi_id: String,
35    position: Arc<Mutex<Point>>,
36    started: Arc<Mutex<bool>>,
37    properties: PointerCommonProperties,
38}
39
40impl<OT: ConnectionTransport> TouchHandle<OT> {
41    pub(crate) fn new(
42        session: Arc<Mutex<Session<OT>>>,
43        touchscreen: Arc<Touchscreen<OT>>,
44        id: usize,
45        x: f64,
46        y: f64,
47    ) -> Self {
48        let properties = PointerCommonProperties {
49            width: Some(1),      // 2 times default touch radius (0.5 * 2)
50            height: Some(1),     // 2 times default touch radius (0.5 * 2)
51            pressure: Some(0.5),
52            tangential_pressure: None,
53            twist: None,
54            altitude_angle: Some(std::f64::consts::PI / 2.0),
55            azimuth_angle: None,
56        };
57
58        Self {
59            session,
60            touchscreen,
61            id,
62            bidi_id: format!("{}_{}", FINGER_ID_PREFIX, id),
63            position: Arc::new(Mutex::new(Point { x: x.round(), y: y.round() })),
64            started: Arc::new(Mutex::new(false)),
65            properties,
66        }
67    }
68
69    /// Start the touch at the handle's initial position.
70    ///
71    /// This performs a touch down event at the coordinates specified when the handle was created.
72    /// Must be called before `move_to` or `end`.
73    ///
74    /// # Errors
75    /// Returns `InputError::TouchAlreadyStarted` if this handle has already been started.
76    pub async fn start(
77        &self,
78        context: &BrowsingContext,
79        options: Option<TouchMoveOptions>,
80    ) -> Result<(), InputError> {
81        let mut started = self.started.lock().await;
82        if *started {
83            return Err(InputError::TouchAlreadyStarted);
84        }
85
86        let options = options.unwrap_or_default();
87        let position = *self.position.lock().await;
88
89        let command = InputCommand::PerformActions(PerformActions {
90            method: InputPerformActionsMethod::InputPerformActions,
91            params: PerformActionsParameters {
92                context: context.clone(),
93                actions: vec![SourceActions::PointerSourceActions(PointerSourceActions {
94                    r#type: PointerEnum::Pointer,
95                    id: self.bidi_id.clone(),
96                    parameters: Some(PointerParameters {
97                        pointer_type: Some(PointerType::Touch),
98                    }),
99                    actions: vec![
100                        PointerSourceAction::PointerMoveAction(PointerMoveAction {
101                            r#type: PointerMoveEnum::PointerMove,
102                            x: position.x,
103                            y: position.y,
104                            duration: None,
105                            origin: options.origin,
106                            pointer_common_properties: PointerCommonProperties {
107                                width: None,
108                                height: None,
109                                pressure: None,
110                                tangential_pressure: None,
111                                twist: None,
112                                altitude_angle: None,
113                                azimuth_angle: None,
114                            },
115                        }),
116                        PointerSourceAction::PointerDownAction(PointerDownAction {
117                            r#type: PointerDownEnum::PointerDown,
118                            button: 0,
119                            pointer_common_properties: self.properties.clone(),
120                        }),
121                    ],
122                })],
123            },
124        });
125
126        let mut session = self.session.lock().await;
127        session.send(CommandData::InputCommand(command))
128            .await
129            .map_err(|e| InputError::CommandResultError(rustenium_core::error::CommandResultError::SessionSendError(e)))?;
130        *started = true;
131        Ok(())
132    }
133
134    /// Move the touch to a new position.
135    ///
136    /// Simulates dragging the touch point from its current position to the new coordinates.
137    /// The touch must be started first with `start()`.
138    ///
139    /// # Arguments
140    /// * `x` - Target X coordinate in pixels
141    /// * `y` - Target Y coordinate in pixels
142    /// * `context` - The browsing context to perform the touch in
143    pub async fn move_to(
144        &self,
145        x: f64,
146        y: f64,
147        context: &BrowsingContext,
148    ) -> Result<(), InputError> {
149        let new_position = Point { x: x.round(), y: y.round() };
150
151        let command = InputCommand::PerformActions(PerformActions {
152            method: InputPerformActionsMethod::InputPerformActions,
153            params: PerformActionsParameters {
154                context: context.clone(),
155                actions: vec![SourceActions::PointerSourceActions(PointerSourceActions {
156                    r#type: PointerEnum::Pointer,
157                    id: self.bidi_id.clone(),
158                    parameters: Some(PointerParameters {
159                        pointer_type: Some(PointerType::Touch),
160                    }),
161                    actions: vec![PointerSourceAction::PointerMoveAction(PointerMoveAction {
162                        r#type: PointerMoveEnum::PointerMove,
163                        x: new_position.x,
164                        y: new_position.y,
165                        duration: None,
166                        origin: None,
167                        pointer_common_properties: self.properties.clone(),
168                    })],
169                })],
170            },
171        });
172
173        *self.position.lock().await = new_position;
174
175        let mut session = self.session.lock().await;
176        session.send(CommandData::InputCommand(command))
177            .await
178            .map_err(|e| InputError::CommandResultError(rustenium_core::error::CommandResultError::SessionSendError(e)))?;
179        Ok(())
180    }
181
182    /// End the touch by releasing it.
183    ///
184    /// This performs a touch up event, completing the touch gesture. After calling this method,
185    /// the handle is automatically removed from the touchscreen and cannot be reused.
186    pub async fn end(&self, context: &BrowsingContext) -> Result<(), InputError> {
187        let command = InputCommand::PerformActions(PerformActions {
188            method: InputPerformActionsMethod::InputPerformActions,
189            params: PerformActionsParameters {
190                context: context.clone(),
191                actions: vec![SourceActions::PointerSourceActions(PointerSourceActions {
192                    r#type: PointerEnum::Pointer,
193                    id: self.bidi_id.clone(),
194                    parameters: Some(PointerParameters {
195                        pointer_type: Some(PointerType::Touch),
196                    }),
197                    actions: vec![PointerSourceAction::PointerUpAction(PointerUpAction {
198                        r#type: PointerUpEnum::PointerUp,
199                        button: 0,
200                    })],
201                })],
202            },
203        });
204
205        let mut session = self.session.lock().await;
206        session.send(CommandData::InputCommand(command))
207            .await
208            .map_err(|e| InputError::CommandResultError(rustenium_core::error::CommandResultError::SessionSendError(e)))?;
209
210        // Remove this handle from the touchscreen
211        self.touchscreen.remove_handle(self.id).await;
212        Ok(())
213    }
214}
215
216/// BiDi Touchscreen implementation for simulating multi-touch gestures.
217///
218/// `Touchscreen` manages multiple touch points simultaneously, allowing you to simulate
219/// complex multi-touch gestures like pinch-to-zoom, multi-finger swipes, and other
220/// touch interactions.
221///
222/// # Examples
223///
224/// ```no_run
225/// # use rustenium::input::{Touchscreen, TouchMoveOptions};
226/// # use rustenium_bidi_commands::browsing_context::types::BrowsingContext;
227/// # use std::sync::Arc;
228/// # use tokio::sync::Mutex;
229/// # use rustenium_core::Session;
230/// # async fn example(session: Arc<Mutex<Session<rustenium_core::transport::WebsocketConnectionTransport>>>, context: BrowsingContext) -> Result<(), Box<dyn std::error::Error>> {
231/// let touchscreen = Arc::new(Touchscreen::new(session));
232///
233/// // Simulate a pinch gesture with two fingers
234/// let touch1 = touchscreen.touch_start(100.0, 200.0, &context, None).await?;
235/// let touch2 = touchscreen.touch_start(300.0, 200.0, &context, None).await?;
236///
237/// // Move fingers closer together
238/// touch1.move_to(150.0, 200.0, &context).await?;
239/// touch2.move_to(250.0, 200.0, &context).await?;
240///
241/// // Release both touches
242/// touch1.end(&context).await?;
243/// touch2.end(&context).await?;
244/// # Ok(())
245/// # }
246/// ```
247pub struct Touchscreen<OT: ConnectionTransport> {
248    session: Arc<Mutex<Session<OT>>>,
249    touches: Arc<Mutex<Vec<usize>>>,
250    id_counter: Arc<Mutex<usize>>,
251}
252
253impl<OT: ConnectionTransport> Touchscreen<OT> {
254    /// Creates a new Touchscreen instance.
255    pub fn new(session: Arc<Mutex<Session<OT>>>) -> Self {
256        Self {
257            session,
258            touches: Arc::new(Mutex::new(Vec::new())),
259            id_counter: Arc::new(Mutex::new(0)),
260        }
261    }
262
263    /// Start a new touch at the given position and return a handle to control it.
264    ///
265    /// Creates a new touch point and immediately performs a touch down event at the specified
266    /// coordinates. Returns a `TouchHandle` that can be used to move or end the touch.
267    ///
268    /// # Arguments
269    /// * `x` - X coordinate in pixels where the touch starts
270    /// * `y` - Y coordinate in pixels where the touch starts
271    /// * `context` - The browsing context to perform the touch in
272    /// * `options` - Optional touch movement options
273    ///
274    /// # Returns
275    /// A `TouchHandle` that represents this touch point and can be used to control its movement.
276    pub async fn touch_start(
277        self: &Arc<Self>,
278        x: f64,
279        y: f64,
280        context: &BrowsingContext,
281        options: Option<TouchMoveOptions>,
282    ) -> Result<TouchHandle<OT>, InputError> {
283        let mut counter = self.id_counter.lock().await;
284        let id = *counter;
285        *counter += 1;
286        drop(counter);
287
288        let touch = TouchHandle::new(
289            self.session.clone(),
290            self.clone(),
291            id,
292            x,
293            y,
294        );
295
296        touch.start(context, options).await?;
297
298        let mut touches = self.touches.lock().await;
299        touches.push(id);
300
301        Ok(touch)
302    }
303
304    /// Remove a touch handle (called internally by TouchHandle::end)
305    pub(crate) async fn remove_handle(&self, id: usize) {
306        let mut touches = self.touches.lock().await;
307        touches.retain(|&touch_id| touch_id != id);
308    }
309}
310
311impl<OT: ConnectionTransport> Clone for Touchscreen<OT> {
312    fn clone(&self) -> Self {
313        Self {
314            session: self.session.clone(),
315            touches: self.touches.clone(),
316            id_counter: self.id_counter.clone(),
317        }
318    }
319}