Skip to main content

appscale_core/
platform_ios.rs

1//! iOS Mobile Platform Bridge
2//!
3//! Implements PlatformBridge for iOS using UIKit concepts.
4//! At this stage, this is a scaffold that records operations and will
5//! later connect to the actual Swift/ObjC bridge via UniFFI bindings.
6//!
7//! Architecture:
8//!   Rust PlatformBridge → UniFFI FFI → Swift UIKit wrapper
9//!
10//! The iOS bridge handles UIView hierarchy, Core Text measurement,
11//! and iOS-specific capabilities like Haptics, Biometrics, and NativeShare.
12
13use crate::platform::*;
14use crate::tree::NodeId;
15use std::sync::Mutex;
16use std::collections::HashMap;
17
18/// iOS platform bridge.
19///
20/// In production, each method will call through UniFFI to the Swift/ObjC layer.
21/// For now, we maintain an in-memory view registry for testing and development.
22pub struct IosPlatform {
23    next_handle: Mutex<u64>,
24    views: Mutex<HashMap<u64, ViewRecord>>,
25    screen: ScreenSize,
26    scale: f32,
27}
28
29struct ViewRecord {
30    view_type: ViewType,
31    node_id: NodeId,
32    children: Vec<NativeHandle>,
33}
34
35impl IosPlatform {
36    pub fn new() -> Self {
37        Self {
38            next_handle: Mutex::new(1),
39            views: Mutex::new(HashMap::new()),
40            // Default: iPhone 15 logical resolution
41            screen: ScreenSize { width: 390.0, height: 844.0 },
42            scale: 3.0,
43        }
44    }
45
46    pub fn with_screen(mut self, width: f32, height: f32, scale: f32) -> Self {
47        self.screen = ScreenSize { width, height };
48        self.scale = scale;
49        self
50    }
51
52    pub fn view_count(&self) -> usize {
53        self.views.lock().unwrap().len()
54    }
55}
56
57impl Default for IosPlatform {
58    fn default() -> Self { Self::new() }
59}
60
61impl PlatformBridge for IosPlatform {
62    fn platform_id(&self) -> PlatformId {
63        PlatformId::Ios
64    }
65
66    fn create_view(&self, view_type: ViewType, node_id: NodeId) -> NativeHandle {
67        let mut h = self.next_handle.lock().unwrap();
68        let handle = NativeHandle(*h);
69        *h += 1;
70
71        self.views.lock().unwrap().insert(handle.0, ViewRecord {
72            view_type,
73            node_id,
74            children: Vec::new(),
75        });
76
77        handle
78    }
79
80    fn update_view(&self, handle: NativeHandle, _props: &PropsDiff) -> Result<(), PlatformError> {
81        let views = self.views.lock().unwrap();
82        if !views.contains_key(&handle.0) {
83            return Err(PlatformError::ViewNotFound(handle));
84        }
85        // In production: forward props to UIView via UniFFI → Swift
86        Ok(())
87    }
88
89    fn remove_view(&self, handle: NativeHandle) {
90        self.views.lock().unwrap().remove(&handle.0);
91    }
92
93    fn insert_child(&self, parent: NativeHandle, child: NativeHandle, index: usize) {
94        let mut views = self.views.lock().unwrap();
95        if let Some(parent_record) = views.get_mut(&parent.0) {
96            let idx = index.min(parent_record.children.len());
97            parent_record.children.insert(idx, child);
98        }
99    }
100
101    fn remove_child(&self, parent: NativeHandle, child: NativeHandle) {
102        let mut views = self.views.lock().unwrap();
103        if let Some(parent_record) = views.get_mut(&parent.0) {
104            parent_record.children.retain(|c| *c != child);
105        }
106    }
107
108    fn measure_text(&self, text: &str, style: &TextStyle, max_width: f32) -> TextMetrics {
109        // Approximate measurement until connected to Core Text via UniFFI.
110        // Uses SF Pro metrics as baseline (iOS system font).
111        let font_size = style.font_size.unwrap_or(17.0); // iOS default (UIFont.systemFontSize)
112        let char_width = font_size * 0.55; // SF Pro average
113        let line_height = style.line_height.unwrap_or(font_size * 1.2);
114        let total_width = char_width * text.len() as f32;
115
116        let lines = if max_width > 0.0 && total_width > max_width {
117            (total_width / max_width).ceil() as u32
118        } else {
119            1
120        };
121
122        TextMetrics {
123            width: if lines > 1 { max_width } else { total_width },
124            height: line_height * lines as f32,
125            baseline: font_size * 0.75,
126            line_count: lines,
127        }
128    }
129
130    fn screen_size(&self) -> ScreenSize {
131        self.screen
132    }
133
134    fn scale_factor(&self) -> f32 {
135        self.scale
136    }
137
138    fn supports(&self, capability: PlatformCapability) -> bool {
139        matches!(capability,
140            PlatformCapability::Haptics
141            | PlatformCapability::Biometrics
142            | PlatformCapability::PushNotifications
143            | PlatformCapability::NativeDatePicker
144            | PlatformCapability::NativeShare
145            | PlatformCapability::BackgroundFetch
146            | PlatformCapability::ContextMenu
147        )
148    }
149}
150
151#[cfg(test)]
152mod tests {
153    use super::*;
154
155    #[test]
156    fn test_ios_create_and_remove() {
157        let platform = IosPlatform::new();
158        assert_eq!(platform.platform_id(), PlatformId::Ios);
159
160        let h1 = platform.create_view(ViewType::Container, NodeId(1));
161        let h2 = platform.create_view(ViewType::Text, NodeId(2));
162        assert_eq!(platform.view_count(), 2);
163
164        platform.insert_child(h1, h2, 0);
165        platform.remove_child(h1, h2);
166        platform.remove_view(h2);
167        assert_eq!(platform.view_count(), 1);
168    }
169
170    #[test]
171    fn test_ios_capabilities() {
172        let platform = IosPlatform::new();
173        assert!(platform.supports(PlatformCapability::Haptics));
174        assert!(platform.supports(PlatformCapability::Biometrics));
175        assert!(platform.supports(PlatformCapability::PushNotifications));
176        assert!(platform.supports(PlatformCapability::NativeShare));
177        assert!(!platform.supports(PlatformCapability::MenuBar));
178        assert!(!platform.supports(PlatformCapability::SystemTray));
179        assert!(!platform.supports(PlatformCapability::MultiWindow));
180    }
181
182    #[test]
183    fn test_ios_screen_info() {
184        let platform = IosPlatform::new();
185        let screen = platform.screen_size();
186        assert_eq!(screen.width, 390.0);
187        assert_eq!(screen.height, 844.0);
188        assert_eq!(platform.scale_factor(), 3.0);
189    }
190
191    #[test]
192    fn test_ios_custom_screen() {
193        // iPad Pro 12.9"
194        let platform = IosPlatform::new().with_screen(1024.0, 1366.0, 2.0);
195        let screen = platform.screen_size();
196        assert_eq!(screen.width, 1024.0);
197        assert_eq!(screen.height, 1366.0);
198        assert_eq!(platform.scale_factor(), 2.0);
199    }
200
201    #[test]
202    fn test_ios_text_measurement() {
203        let platform = IosPlatform::new();
204        let style = TextStyle { font_size: Some(16.0), ..TextStyle::default() };
205        let metrics = platform.measure_text("Hello iOS", &style, 200.0);
206        assert!(metrics.width > 0.0);
207        assert!(metrics.height > 0.0);
208        assert_eq!(metrics.line_count, 1);
209    }
210
211    #[test]
212    fn test_ios_text_wrapping() {
213        let platform = IosPlatform::new();
214        let style = TextStyle { font_size: Some(16.0), ..TextStyle::default() };
215        // Long text that should wrap in a narrow container
216        let metrics = platform.measure_text("This is a longer text that should wrap", &style, 50.0);
217        assert!(metrics.line_count > 1);
218        assert_eq!(metrics.width, 50.0); // Should fill max_width when wrapping
219    }
220
221    #[test]
222    fn test_ios_update_missing_view() {
223        let platform = IosPlatform::new();
224        let result = platform.update_view(NativeHandle(999), &PropsDiff::new());
225        assert!(result.is_err());
226    }
227}