Skip to main content

appscale_core/
platform_macos.rs

1//! macOS Desktop Platform Bridge
2//!
3//! Implements PlatformBridge for macOS using AppKit concepts.
4//! At this stage, this is a scaffold that records operations and will
5//! later connect to the actual Objective-C/Swift bridge via FFI.
6//!
7//! Architecture:
8//!   Rust PlatformBridge → FFI (C ABI) → Swift/ObjC AppKit wrapper
9//!
10//! The macOS bridge handles NSView hierarchy, NSTextField measurement,
11//! and macOS-specific capabilities like MenuBar, SystemTray, and DragAndDrop.
12
13use crate::platform::*;
14use crate::tree::NodeId;
15use std::sync::Mutex;
16use std::collections::HashMap;
17
18/// macOS platform bridge.
19///
20/// In production, each method will call through FFI to the Swift/ObjC layer.
21/// For now, we maintain an in-memory view registry for testing and development.
22pub struct MacosPlatform {
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 MacosPlatform {
36    pub fn new() -> Self {
37        Self {
38            next_handle: Mutex::new(1),
39            views: Mutex::new(HashMap::new()),
40            // Default: 14" MacBook Pro logical resolution
41            screen: ScreenSize { width: 1512.0, height: 982.0 },
42            scale: 2.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 MacosPlatform {
58    fn default() -> Self { Self::new() }
59}
60
61impl PlatformBridge for MacosPlatform {
62    fn platform_id(&self) -> PlatformId {
63        PlatformId::Macos
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 NSView via FFI
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 FFI.
110        // Uses system font metrics as baseline (SF Pro).
111        let font_size = style.font_size.unwrap_or(13.0); // macOS default
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.8,
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::MenuBar
141            | PlatformCapability::SystemTray
142            | PlatformCapability::MultiWindow
143            | PlatformCapability::DragAndDrop
144            | PlatformCapability::ContextMenu
145            | PlatformCapability::NativeFilePicker
146            | PlatformCapability::NativeDatePicker
147        )
148    }
149}
150
151#[cfg(test)]
152mod tests {
153    use super::*;
154
155    #[test]
156    fn test_macos_create_and_remove() {
157        let platform = MacosPlatform::new();
158        assert_eq!(platform.platform_id(), PlatformId::Macos);
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_macos_capabilities() {
172        let platform = MacosPlatform::new();
173        assert!(platform.supports(PlatformCapability::MenuBar));
174        assert!(platform.supports(PlatformCapability::MultiWindow));
175        assert!(platform.supports(PlatformCapability::DragAndDrop));
176        assert!(!platform.supports(PlatformCapability::Haptics));
177        assert!(!platform.supports(PlatformCapability::Biometrics));
178    }
179
180    #[test]
181    fn test_macos_screen_info() {
182        let platform = MacosPlatform::new();
183        let screen = platform.screen_size();
184        assert_eq!(screen.width, 1512.0);
185        assert_eq!(screen.height, 982.0);
186        assert_eq!(platform.scale_factor(), 2.0);
187    }
188
189    #[test]
190    fn test_macos_text_measurement() {
191        let platform = MacosPlatform::new();
192        let style = TextStyle { font_size: Some(16.0), ..TextStyle::default() };
193        let metrics = platform.measure_text("Hello macOS", &style, 200.0);
194        assert!(metrics.width > 0.0);
195        assert!(metrics.height > 0.0);
196        assert_eq!(metrics.line_count, 1);
197    }
198
199    #[test]
200    fn test_macos_update_missing_view() {
201        let platform = MacosPlatform::new();
202        let result = platform.update_view(NativeHandle(999), &PropsDiff::new());
203        assert!(result.is_err());
204    }
205}