Skip to main content

uzor_mobile/
lib.rs

1//! Mobile backend for uzor supporting iOS and Android
2//!
3//! This crate provides the mobile platform implementation for uzor,
4//! supporting iOS and Android devices with touch-first design.
5//!
6//! # Architecture
7//!
8//! The mobile backend provides:
9//! - Touch input handling (multi-touch)
10//! - Virtual keyboard integration (IME)
11//! - Mobile-specific features (haptics, orientation, safe areas)
12//! - Native clipboard integration
13//! - System theme detection
14//!
15//! # Platform-Specific Notes
16//!
17//! ## iOS
18//! - Uses UIKit for native UI integration
19//! - Clipboard via UIPasteboard
20//! - Theme detection via UITraitCollection
21//! - Safe area insets for notch/home indicator
22//!
23//! ## Android
24//! - Uses Android NDK and JNI
25//! - Clipboard via ClipboardManager
26//! - Theme detection via Configuration.UI_MODE_NIGHT
27//! - System bars handling
28//!
29//! # Feature Flags
30//!
31//! - `android`: Enable Android-specific functionality
32//! - `ios`: Enable iOS-specific functionality
33//!
34//! # Example
35//!
36//! ```ignore
37//! use uzor_mobile::MobilePlatform;
38//! use uzor::platform::{PlatformBackend, WindowConfig};
39//!
40//! fn main() {
41//!     let mut platform = MobilePlatform::new().unwrap();
42//!
43//!     let window = platform.create_window(
44//!         WindowConfig::new("My Mobile App")
45//!     ).unwrap();
46//!
47//!     // Handle touch events
48//!     while let Some(event) = platform.poll_event() {
49//!         match event {
50//!             PlatformEvent::TouchStart { id, x, y } => {
51//!                 // Handle touch start
52//!             }
53//!             PlatformEvent::TouchMove { id, x, y } => {
54//!                 // Handle touch move
55//!             }
56//!             PlatformEvent::TouchEnd { id, x, y } => {
57//!                 // Handle touch end
58//!             }
59//!             _ => {}
60//!         }
61//!     }
62//! }
63//! ```
64
65#![allow(dead_code)]
66
67pub use uzor;
68
69use std::collections::VecDeque;
70use std::sync::{Arc, Mutex};
71
72use uzor::platform::{
73    backends::PlatformBackend,
74    types::{PlatformError, WindowId, SystemIntegration},
75    PlatformEvent, SystemTheme, WindowConfig,
76};
77
78// Platform-specific modules
79#[cfg(target_os = "android")]
80mod android;
81#[cfg(target_os = "android")]
82use android::AndroidBackend;
83
84#[cfg(target_os = "ios")]
85mod ios;
86#[cfg(target_os = "ios")]
87use ios::IosBackend;
88
89mod common;
90
91// =============================================================================
92// Mobile Platform
93// =============================================================================
94
95/// Mobile platform backend for uzor
96///
97/// Provides a unified interface for iOS and Android platforms.
98/// Uses platform-specific implementations internally based on target OS.
99pub struct MobilePlatform {
100    state: Arc<Mutex<MobileState>>,
101}
102
103struct MobileState {
104    /// Active window (mobile apps typically have only one)
105    window: Option<MobileWindow>,
106
107    /// Platform-specific backend
108    #[cfg(target_os = "android")]
109    backend: AndroidBackend,
110    #[cfg(target_os = "ios")]
111    backend: IosBackend,
112    #[cfg(not(any(target_os = "android", target_os = "ios")))]
113    backend: StubBackend,
114
115    /// Event queue
116    event_queue: VecDeque<PlatformEvent>,
117
118    /// IME state
119    ime_position: (f64, f64),
120    ime_allowed: bool,
121}
122
123struct MobileWindow {
124    id: WindowId,
125    config: WindowConfig,
126    width: u32,
127    height: u32,
128    scale_factor: f64,
129}
130
131impl MobilePlatform {
132    /// Create a new mobile platform instance
133    ///
134    /// # Errors
135    ///
136    /// Returns an error if the platform-specific backend fails to initialize.
137    pub fn new() -> Result<Self, PlatformError> {
138        #[cfg(target_os = "android")]
139        let backend = AndroidBackend::new()
140            .map_err(|e| PlatformError::CreationFailed(format!("Android backend init failed: {}", e)))?;
141
142        #[cfg(target_os = "ios")]
143        let backend = IosBackend::new()
144            .map_err(|e| PlatformError::CreationFailed(format!("iOS backend init failed: {}", e)))?;
145
146        #[cfg(not(any(target_os = "android", target_os = "ios")))]
147        let backend = StubBackend::new();
148
149        Ok(Self {
150            state: Arc::new(Mutex::new(MobileState {
151                window: None,
152                backend,
153                event_queue: VecDeque::new(),
154                ime_position: (0.0, 0.0),
155                ime_allowed: false,
156            })),
157        })
158    }
159
160    /// Get safe area insets (for notch, home indicator, etc.)
161    ///
162    /// Returns (top, right, bottom, left) insets in physical pixels.
163    pub fn safe_area_insets(&self) -> (f64, f64, f64, f64) {
164        let state = self.state.lock().unwrap();
165        state.backend.safe_area_insets()
166    }
167
168    /// Get current screen orientation
169    pub fn orientation(&self) -> ScreenOrientation {
170        let state = self.state.lock().unwrap();
171        state.backend.orientation()
172    }
173
174    /// Trigger haptic feedback
175    ///
176    /// # Arguments
177    ///
178    /// * `style` - The haptic feedback style to trigger
179    pub fn haptic_feedback(&mut self, style: HapticStyle) {
180        let mut state = self.state.lock().unwrap();
181        state.backend.haptic_feedback(style);
182    }
183}
184
185impl Default for MobilePlatform {
186    fn default() -> Self {
187        Self::new().expect("Failed to create mobile platform")
188    }
189}
190
191// =============================================================================
192// PlatformBackend Implementation
193// =============================================================================
194
195impl PlatformBackend for MobilePlatform {
196    fn name(&self) -> &'static str {
197        todo!("not yet implemented for this platform")
198    }
199
200    fn create_window(&mut self, config: WindowConfig) -> Result<WindowId, PlatformError> {
201        let mut state = self.state.lock().unwrap();
202
203        // Mobile apps typically have only one window
204        if state.window.is_some() {
205            return Err(PlatformError::CreationFailed(
206                "Mobile platform supports only one window".to_string(),
207            ));
208        }
209
210        let window_id = WindowId::new();
211
212        // Get screen size from backend
213        let (width, height) = state.backend.screen_size();
214        let scale_factor = state.backend.scale_factor();
215
216        let window = MobileWindow {
217            id: window_id,
218            config,
219            width,
220            height,
221            scale_factor,
222        };
223
224        state.window = Some(window);
225        state.event_queue.push_back(PlatformEvent::WindowCreated);
226
227        Ok(window_id)
228    }
229
230    fn close_window(&mut self, window_id: WindowId) -> Result<(), PlatformError> {
231        let mut state = self.state.lock().unwrap();
232
233        if let Some(window) = &state.window {
234            if window.id == window_id {
235                state.window = None;
236                state.event_queue.push_back(PlatformEvent::WindowDestroyed);
237                return Ok(());
238            }
239        }
240
241        Err(PlatformError::WindowNotFound)
242    }
243
244    fn primary_window(&self) -> Option<WindowId> {
245        todo!("not yet implemented for this platform")
246    }
247
248    fn poll_events(&mut self) -> Vec<PlatformEvent> {
249        todo!("not yet implemented for this platform")
250    }
251
252    fn request_redraw(&self, id: WindowId) {
253        let _ = id;
254        // No-op for now: mobile redraws are driven by the OS event loop
255    }
256}
257
258// =============================================================================
259// SystemIntegration Implementation
260// =============================================================================
261
262impl SystemIntegration for MobilePlatform {
263    fn get_clipboard(&self) -> Option<String> {
264        todo!("not yet implemented for this platform")
265    }
266
267    fn set_clipboard(&self, _text: &str) {
268        todo!("not yet implemented for this platform")
269    }
270
271    fn get_system_theme(&self) -> Option<SystemTheme> {
272        let state = self.state.lock().unwrap();
273        state.backend.system_theme()
274    }
275}
276
277// =============================================================================
278// Mobile-Specific Types
279// =============================================================================
280
281/// Screen orientation
282#[derive(Clone, Copy, Debug, PartialEq, Eq)]
283pub enum ScreenOrientation {
284    /// Portrait (vertical)
285    Portrait,
286    /// Landscape (horizontal)
287    Landscape,
288    /// Portrait upside down
289    PortraitUpsideDown,
290    /// Landscape right
291    LandscapeRight,
292}
293
294/// Haptic feedback style
295#[derive(Clone, Copy, Debug, PartialEq, Eq)]
296pub enum HapticStyle {
297    /// Light impact (subtle)
298    Light,
299    /// Medium impact
300    Medium,
301    /// Heavy impact
302    Heavy,
303    /// Selection feedback (tick)
304    Selection,
305    /// Success feedback
306    Success,
307    /// Warning feedback
308    Warning,
309    /// Error feedback
310    Error,
311}
312
313// =============================================================================
314// Stub Backend (for non-mobile platforms during development)
315// =============================================================================
316
317#[cfg(not(any(target_os = "android", target_os = "ios")))]
318struct StubBackend;
319
320#[cfg(not(any(target_os = "android", target_os = "ios")))]
321impl StubBackend {
322    fn new() -> Self {
323        StubBackend
324    }
325
326    fn screen_size(&self) -> (u32, u32) {
327        (800, 600)
328    }
329
330    fn scale_factor(&self) -> f64 {
331        1.0
332    }
333
334    fn safe_area_insets(&self) -> (f64, f64, f64, f64) {
335        (0.0, 0.0, 0.0, 0.0)
336    }
337
338    fn orientation(&self) -> ScreenOrientation {
339        ScreenOrientation::Portrait
340    }
341
342    fn haptic_feedback(&mut self, _style: HapticStyle) {}
343
344    fn poll_event(&mut self) -> Option<PlatformEvent> {
345        None
346    }
347
348    fn set_title(&mut self, _title: &str) {}
349
350    fn get_clipboard_text(&self) -> Option<String> {
351        None
352    }
353
354    fn set_clipboard_text(&mut self, _text: &str) -> Result<(), String> {
355        Err("Clipboard not available on stub backend".to_string())
356    }
357
358    fn open_url(&self, _url: &str) -> Result<(), String> {
359        Err("URL opening not available on stub backend".to_string())
360    }
361
362    fn system_theme(&self) -> Option<SystemTheme> {
363        Some(SystemTheme::Light)
364    }
365
366    fn set_ime_position(&mut self, _x: f64, _y: f64) {}
367
368    fn show_keyboard(&mut self) {}
369
370    fn hide_keyboard(&mut self) {}
371}
372
373// =============================================================================
374// Tests
375// =============================================================================
376
377#[cfg(test)]
378mod tests {
379    use super::*;
380
381    #[test]
382    fn test_haptic_style_variants() {
383        let styles = vec![
384            HapticStyle::Light,
385            HapticStyle::Medium,
386            HapticStyle::Heavy,
387            HapticStyle::Selection,
388            HapticStyle::Success,
389            HapticStyle::Warning,
390            HapticStyle::Error,
391        ];
392
393        assert_eq!(styles.len(), 7);
394    }
395
396    #[test]
397    fn test_screen_orientation_variants() {
398        let orientations = vec![
399            ScreenOrientation::Portrait,
400            ScreenOrientation::Landscape,
401            ScreenOrientation::PortraitUpsideDown,
402            ScreenOrientation::LandscapeRight,
403        ];
404
405        assert_eq!(orientations.len(), 4);
406    }
407
408    #[cfg(not(any(target_os = "android", target_os = "ios")))]
409    #[test]
410    fn test_stub_backend() {
411        let backend = StubBackend::new();
412
413        assert_eq!(backend.screen_size(), (800, 600));
414        assert_eq!(backend.scale_factor(), 1.0);
415        assert_eq!(backend.safe_area_insets(), (0.0, 0.0, 0.0, 0.0));
416        assert_eq!(backend.orientation(), ScreenOrientation::Portrait);
417        assert_eq!(backend.get_clipboard_text(), None);
418        assert_eq!(backend.system_theme(), Some(SystemTheme::Light));
419    }
420}