Skip to main content

appscale_core/
platform.rs

1//! Platform Bridge — trait-based contracts for native platform integration.
2//!
3//! Each platform (iOS, Android, macOS, Windows, Web) implements PlatformBridge.
4//! The consistency layer uses capability queries (not lowest-common-denominator)
5//! to enable platform-adaptive rendering.
6
7use crate::tree::NodeId;
8use serde::{Serialize, Deserialize};
9use std::collections::HashMap;
10
11// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
12// Core platform bridge trait
13// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
14
15/// Every platform bridge MUST implement this trait.
16pub trait PlatformBridge: Send + Sync {
17    fn platform_id(&self) -> PlatformId;
18
19    // --- View lifecycle ---
20    fn create_view(&self, view_type: ViewType, node_id: NodeId) -> NativeHandle;
21    fn update_view(&self, handle: NativeHandle, props: &PropsDiff) -> Result<(), PlatformError>;
22    fn remove_view(&self, handle: NativeHandle);
23    fn insert_child(&self, parent: NativeHandle, child: NativeHandle, index: usize);
24    fn remove_child(&self, parent: NativeHandle, child: NativeHandle);
25
26    // --- Text measurement (required by Taffy for layout) ---
27    fn measure_text(&self, text: &str, style: &TextStyle, max_width: f32) -> TextMetrics;
28
29    // --- Screen info ---
30    fn screen_size(&self) -> ScreenSize;
31    fn scale_factor(&self) -> f32;
32
33    // --- Capability queries ---
34    fn supports(&self, capability: PlatformCapability) -> bool;
35}
36
37// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
38// Types
39// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
40
41#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
42pub enum PlatformId { Ios, Android, Macos, Windows, Web }
43
44/// Opaque handle to a platform-native view.
45#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
46pub struct NativeHandle(pub u64);
47
48/// View types the framework knows how to create.
49#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
50pub enum ViewType {
51    Container,
52    Text,
53    TextInput,
54    Image,
55    ScrollView,
56    Button,
57    Switch,
58    Slider,
59    ActivityIndicator,
60    DatePicker,
61    Modal,
62    BottomSheet,
63    MenuBar,
64    TitleBar,
65    Custom(String),
66}
67
68#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
69pub enum PlatformCapability {
70    Haptics,
71    Biometrics,
72    MenuBar,
73    SystemTray,
74    MultiWindow,
75    DragAndDrop,
76    ContextMenu,
77    NativeShare,
78    PushNotifications,
79    BackgroundFetch,
80    NativeDatePicker,
81    NativeFilePicker,
82}
83
84// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
85// Props
86// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
87
88/// A single property value.
89#[derive(Debug, Clone, Serialize, Deserialize)]
90#[serde(untagged)]
91pub enum PropValue {
92    String(String),
93    F32(f32),
94    F64(f64),
95    I32(i32),
96    Bool(bool),
97    Color(Color),
98    Rect { x: f32, y: f32, width: f32, height: f32 },
99    Null,
100}
101
102/// A diff of changed properties.
103#[derive(Debug, Clone, Default, Serialize, Deserialize)]
104pub struct PropsDiff {
105    pub changes: HashMap<String, PropValue>,
106}
107
108impl PropsDiff {
109    pub fn new() -> Self { Self::default() }
110
111    pub fn set(&mut self, key: impl Into<String>, value: PropValue) {
112        self.changes.insert(key.into(), value);
113    }
114
115    pub fn is_empty(&self) -> bool { self.changes.is_empty() }
116}
117
118#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
119pub struct Color {
120    pub r: u8,
121    pub g: u8,
122    pub b: u8,
123    pub a: f32,
124}
125
126impl Color {
127    pub fn rgb(r: u8, g: u8, b: u8) -> Self {
128        Self { r, g, b, a: 1.0 }
129    }
130
131    pub fn rgba(r: u8, g: u8, b: u8, a: f32) -> Self {
132        Self { r, g, b, a }
133    }
134}
135
136// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
137// Text
138// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
139
140#[derive(Debug, Clone, Default, Serialize, Deserialize)]
141pub struct TextStyle {
142    pub font_family: Option<String>,
143    pub font_size: Option<f32>,
144    pub font_weight: Option<FontWeight>,
145    pub color: Option<Color>,
146    pub line_height: Option<f32>,
147    pub letter_spacing: Option<f32>,
148    pub text_align: Option<TextAlign>,
149}
150
151#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
152pub enum FontWeight { Thin, Light, Regular, Medium, SemiBold, Bold, Heavy }
153
154#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
155pub enum TextAlign { Left, Center, Right, Justify }
156
157#[derive(Debug, Clone, Copy, Default)]
158pub struct TextMetrics {
159    pub width: f32,
160    pub height: f32,
161    pub baseline: f32,
162    pub line_count: u32,
163}
164
165#[derive(Debug, Clone, Copy)]
166pub struct ScreenSize {
167    pub width: f32,
168    pub height: f32,
169}
170
171// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
172// Errors
173// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
174
175#[derive(Debug, thiserror::Error)]
176pub enum PlatformError {
177    #[error("View not found: {0:?}")]
178    ViewNotFound(NativeHandle),
179
180    #[error("Unsupported view type: {0:?}")]
181    UnsupportedViewType(ViewType),
182
183    #[error("Native error: {0}")]
184    Native(String),
185}
186
187// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
188// Test platform (for unit testing without a real OS)
189// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
190
191/// A mock platform bridge for testing.
192/// Records all operations for assertion.
193#[cfg(test)]
194pub mod mock {
195    use super::*;
196    use std::sync::Mutex;
197
198    #[derive(Debug, Clone)]
199    pub enum MockOp {
200        CreateView(ViewType, NodeId),
201        UpdateView(NativeHandle, PropsDiff),
202        RemoveView(NativeHandle),
203        InsertChild(NativeHandle, NativeHandle, usize),
204        RemoveChild(NativeHandle, NativeHandle),
205    }
206
207    pub struct MockPlatform {
208        next_handle: Mutex<u64>,
209        pub ops: Mutex<Vec<MockOp>>,
210    }
211
212    impl MockPlatform {
213        pub fn new() -> Self {
214            Self {
215                next_handle: Mutex::new(1),
216                ops: Mutex::new(Vec::new()),
217            }
218        }
219    }
220
221    impl PlatformBridge for MockPlatform {
222        fn platform_id(&self) -> PlatformId { PlatformId::Web }
223
224        fn create_view(&self, view_type: ViewType, node_id: NodeId) -> NativeHandle {
225            let mut h = self.next_handle.lock().unwrap();
226            let handle = NativeHandle(*h);
227            *h += 1;
228            self.ops.lock().unwrap().push(MockOp::CreateView(view_type, node_id));
229            handle
230        }
231
232        fn update_view(&self, handle: NativeHandle, props: &PropsDiff) -> Result<(), PlatformError> {
233            self.ops.lock().unwrap().push(MockOp::UpdateView(handle, props.clone()));
234            Ok(())
235        }
236
237        fn remove_view(&self, handle: NativeHandle) {
238            self.ops.lock().unwrap().push(MockOp::RemoveView(handle));
239        }
240
241        fn insert_child(&self, parent: NativeHandle, child: NativeHandle, index: usize) {
242            self.ops.lock().unwrap().push(MockOp::InsertChild(parent, child, index));
243        }
244
245        fn remove_child(&self, parent: NativeHandle, child: NativeHandle) {
246            self.ops.lock().unwrap().push(MockOp::RemoveChild(parent, child));
247        }
248
249        fn measure_text(&self, _text: &str, style: &TextStyle, _max_width: f32) -> TextMetrics {
250            let font_size = style.font_size.unwrap_or(14.0);
251            TextMetrics {
252                width: font_size * 0.6 * _text.len() as f32,
253                height: font_size * 1.2,
254                baseline: font_size,
255                line_count: 1,
256            }
257        }
258
259        fn screen_size(&self) -> ScreenSize {
260            ScreenSize { width: 390.0, height: 844.0 }
261        }
262
263        fn scale_factor(&self) -> f32 { 3.0 }
264
265        fn supports(&self, _cap: PlatformCapability) -> bool { false }
266    }
267}