Skip to main content

waterui_cli/workflows/
gesture.rs

1//! Unified gesture API for device automation.
2//!
3//! Provides a platform-agnostic interface for performing gestures (tap, swipe, text)
4//! on iOS simulators, Android devices, and macOS local machine.
5
6use std::time::Duration;
7
8use crate::capture::{DevicePlatform, detect_platform};
9use crate::diff::DiffResult;
10use crate::toolchain::Host;
11use crate::{android, apple};
12
13/// Special device ID for macOS local machine.
14pub const LOCAL_DEVICE_ID: &str = "local";
15
16/// Options for gesture execution with diff support.
17#[derive(Debug, Clone, Default)]
18pub struct GestureOptions {
19    /// Whether to capture before/after screenshots and compute diff.
20    pub diff: bool,
21    /// Path to save the diff image (only used if diff is true).
22    pub diff_output: Option<std::path::PathBuf>,
23    /// Delay in milliseconds after gesture before capturing "after" screenshot.
24    /// Default is 500ms.
25    pub delay_ms: Option<u32>,
26}
27
28impl GestureOptions {
29    /// Create new gesture options with diff enabled.
30    #[must_use]
31    pub fn with_diff() -> Self {
32        Self {
33            diff: true,
34            ..Default::default()
35        }
36    }
37
38    /// Set the diff output path.
39    #[must_use]
40    pub fn diff_output(mut self, path: impl Into<std::path::PathBuf>) -> Self {
41        self.diff_output = Some(path.into());
42        self
43    }
44
45    /// Set the delay after gesture in milliseconds.
46    #[must_use]
47    pub const fn delay(mut self, ms: u32) -> Self {
48        self.delay_ms = Some(ms);
49        self
50    }
51}
52
53/// Result of a gesture with diff information.
54#[derive(Debug)]
55pub struct GestureResult {
56    /// The diff result if diff was enabled.
57    pub diff: Option<DiffResult>,
58}
59
60/// Capture screenshot bytes for a device.
61async fn capture_screenshot_bytes(host: &Host, device_id: &str) -> eyre::Result<Vec<u8>> {
62    if device_id == LOCAL_DEVICE_ID {
63        apple::local::screenshot_bytes(host).await
64    } else {
65        match detect_platform(device_id) {
66            DevicePlatform::Ios => apple::device::screenshot_bytes(host, device_id).await,
67            DevicePlatform::Android => android::device::screenshot_bytes(host, device_id).await,
68        }
69    }
70}
71
72/// Execute a gesture with optional diff capture.
73async fn execute_with_diff<F, Fut>(
74    host: &Host,
75    device_id: &str,
76    options: &GestureOptions,
77    gesture_fn: F,
78) -> eyre::Result<GestureResult>
79where
80    F: FnOnce() -> Fut,
81    Fut: std::future::Future<Output = eyre::Result<()>>,
82{
83    if !options.diff {
84        // No diff, just execute the gesture
85        gesture_fn().await?;
86        return Ok(GestureResult { diff: None });
87    }
88
89    // Capture before screenshot
90    let before = capture_screenshot_bytes(host, device_id).await?;
91
92    // Execute the gesture
93    gesture_fn().await?;
94
95    // Wait for the specified delay (default 500ms)
96    let delay = options.delay_ms.unwrap_or(500);
97    smol::Timer::after(Duration::from_millis(u64::from(delay))).await;
98
99    // Capture after screenshot
100    let after = capture_screenshot_bytes(host, device_id).await?;
101
102    // Compute diff
103    let diff_result = crate::diff::compare_images(&before, &after)?;
104
105    // Save diff image if requested
106    if let Some(ref output_path) = options.diff_output {
107        crate::diff::save_diff_image(&before, &after, output_path)?;
108    }
109
110    Ok(GestureResult {
111        diff: Some(diff_result),
112    })
113}
114
115/// Perform a tap gesture on a device.
116///
117/// # Arguments
118///
119/// * `device_id` - Device identifier (UDID for iOS, serial for Android, "local" for macOS)
120/// * `x` - X coordinate
121/// * `y` - Y coordinate
122/// * `options` - Gesture options including diff settings
123///
124/// # Errors
125///
126/// Returns an error if the tap fails or the device is not available.
127pub async fn tap(
128    host: &Host,
129    device_id: &str,
130    x: u32,
131    y: u32,
132    options: &GestureOptions,
133) -> eyre::Result<GestureResult> {
134    execute_with_diff(host, device_id, options, || async {
135        if device_id == LOCAL_DEVICE_ID {
136            apple::local::tap(host, x, y).await
137        } else {
138            match detect_platform(device_id) {
139                DevicePlatform::Ios => apple::device::tap(host, device_id, x, y).await,
140                DevicePlatform::Android => android::device::tap(host, device_id, x, y).await,
141            }
142        }
143    })
144    .await
145}
146
147/// Perform a swipe gesture on a device.
148///
149/// # Arguments
150///
151/// * `device_id` - Device identifier
152/// * `from` - Starting coordinates (x, y)
153/// * `to` - Ending coordinates (x, y)
154/// * `duration_ms` - Duration of the swipe in milliseconds
155/// * `options` - Gesture options including diff settings
156///
157/// # Errors
158///
159/// Returns an error if the swipe fails or the device is not available.
160pub async fn swipe(
161    host: &Host,
162    device_id: &str,
163    from: (u32, u32),
164    to: (u32, u32),
165    duration_ms: Option<u32>,
166    options: &GestureOptions,
167) -> eyre::Result<GestureResult> {
168    execute_with_diff(host, device_id, options, || async {
169        if device_id == LOCAL_DEVICE_ID {
170            apple::local::swipe(host, from, to, duration_ms).await
171        } else {
172            match detect_platform(device_id) {
173                DevicePlatform::Ios => {
174                    apple::device::swipe(host, device_id, from, to, duration_ms).await
175                }
176                DevicePlatform::Android => {
177                    android::device::swipe(host, device_id, from, to, duration_ms).await
178                }
179            }
180        }
181    })
182    .await
183}
184
185/// Input text on a device.
186///
187/// # Arguments
188///
189/// * `device_id` - Device identifier
190/// * `input` - Text to input
191/// * `options` - Gesture options including diff settings
192///
193/// # Errors
194///
195/// Returns an error if the text input fails or the device is not available.
196pub async fn text(
197    host: &Host,
198    device_id: &str,
199    input: &str,
200    options: &GestureOptions,
201) -> eyre::Result<GestureResult> {
202    execute_with_diff(host, device_id, options, || async {
203        if device_id == LOCAL_DEVICE_ID {
204            apple::local::text(host, input).await
205        } else {
206            match detect_platform(device_id) {
207                DevicePlatform::Ios => apple::device::text(host, device_id, input).await,
208                DevicePlatform::Android => android::device::text(host, device_id, input).await,
209            }
210        }
211    })
212    .await
213}
214
215/// Verify that a device exists and is available.
216///
217/// # Errors
218///
219/// Returns an error if the device is not found or not available.
220pub async fn verify_device(host: &Host, device_id: &str) -> eyre::Result<DevicePlatform> {
221    if device_id == LOCAL_DEVICE_ID {
222        // Local device is always available on macOS
223        #[cfg(target_os = "macos")]
224        return Ok(DevicePlatform::Ios); // Use iOS platform type for local (AppleScript-based)
225
226        #[cfg(not(target_os = "macos"))]
227        return Err(eyre::eyre!("Local device is only available on macOS"));
228    }
229
230    crate::capture::verify_device(host, device_id).await
231}