Skip to main content

appscale_core/
lib.rs

1//! AppScale Universal Framework — Rust Execution Engine
2//!
3//! This is the "mini operating system" that sits between React's reconciler
4//! and platform-native widgets. It owns:
5//! - UI tree lifecycle (shadow tree)
6//! - Layout computation (Taffy)
7//! - Event routing and gesture recognition
8//! - Binary IR decode/encode
9//! - Platform bridge dispatch
10//!
11//! Design principle: React = intent, Rust = execution.
12
13pub mod ir;
14pub mod platform;
15pub mod layout;
16pub mod events;
17pub mod tree;
18pub mod scheduler;
19pub mod navigation;
20pub mod accessibility;
21pub mod bridge;
22pub mod generated;
23pub mod platform_macos;
24pub mod platform_windows;
25pub mod platform_ios;
26pub mod platform_android;
27pub mod platform_web;
28pub mod modules;
29pub mod devtools;
30pub mod components;
31pub mod storage;
32pub mod ai;
33pub mod cloud;
34pub mod plugins;
35
36/// Re-export core types used across the engine.
37pub mod prelude {
38    pub use crate::ir::{IrCommand, IrBatch};
39    pub use crate::platform::{
40        PlatformBridge, PlatformId, PlatformCapability,
41        NativeHandle, ViewType, PropValue, PropsDiff,
42    };
43    pub use crate::layout::{LayoutEngine, LayoutStyle, ComputedLayout};
44    pub use crate::events::{InputEvent, PointerEvent, KeyboardEvent};
45    pub use crate::tree::{ShadowTree, NodeId};
46    pub use crate::scheduler::{Scheduler, Priority};
47    pub use crate::navigation::{Navigator, NavigationAction, NavigationEvent};
48    pub use crate::accessibility::{AccessibilityInfo, AccessibilityRole, FocusManager};
49    pub use crate::bridge::{SyncCall, SyncResult, AsyncCall, NativeCallback};
50    pub use crate::platform_ios::IosPlatform;
51    pub use crate::platform_android::AndroidPlatform;
52    pub use crate::platform_web::WebPlatform;
53    pub use crate::platform_macos::MacosPlatform;
54    pub use crate::platform_windows::WindowsPlatform;
55}
56
57use tree::ShadowTree;
58use layout::LayoutEngine;
59use events::{EventDispatcher, InputEvent};
60use scheduler::Scheduler;
61use navigation::Navigator;
62use accessibility::FocusManager;
63use bridge::{SyncCall, SyncResult, AsyncCall};
64use platform::PlatformBridge;
65use std::collections::HashSet;
66use std::sync::Arc;
67use std::time::Instant;
68
69/// The Engine is the central coordinator.
70/// One Engine instance per application.
71pub struct Engine {
72    tree: ShadowTree,
73    layout: LayoutEngine,
74    events: EventDispatcher,
75    scheduler: Scheduler,
76    navigator: Navigator,
77    focus: FocusManager,
78    platform: Arc<dyn PlatformBridge>,
79    frame_count: u64,
80
81    /// Dirty tracking: nodes whose layout needs recomputation.
82    /// Only dirty subtrees are recomputed — not the full tree.
83    dirty_nodes: HashSet<tree::NodeId>,
84}
85
86impl Engine {
87    /// Create a new Engine with the given platform bridge.
88    pub fn new(platform: Arc<dyn PlatformBridge>) -> Self {
89        tracing::info!(
90            platform = ?platform.platform_id(),
91            "AppScale Engine initialized"
92        );
93
94        Self {
95            tree: ShadowTree::new(),
96            layout: LayoutEngine::new(),
97            events: EventDispatcher::new(),
98            scheduler: Scheduler::new(),
99            navigator: Navigator::new(),
100            focus: FocusManager::new(),
101            platform,
102            frame_count: 0,
103            dirty_nodes: HashSet::new(),
104        }
105    }
106
107    /// Process a batch of IR commands from the reconciler.
108    /// This is the main entry point — called after every React commit.
109    ///
110    /// The flow:
111    /// 1. Decode IR commands (create/update/delete/move nodes)
112    /// 2. Apply to shadow tree + mark dirty
113    /// 3. Recompute layout ONLY for dirty subtrees (Taffy)
114    /// 4. Diff against previous layout
115    /// 5. Issue platform bridge calls (mount phase)
116    pub fn apply_commit(&mut self, batch: &ir::IrBatch) -> Result<(), EngineError> {
117        self.frame_count += 1;
118        let frame_start = Instant::now();
119        let _span = tracing::info_span!("commit", frame = self.frame_count).entered();
120
121        // Phase 1: Apply IR commands to shadow tree + collect dirty nodes
122        let dirty_nodes = self.apply_ir_to_tree(batch)?;
123        self.dirty_nodes.extend(&dirty_nodes);
124
125        // Phase 2: Recompute layout for dirty subtrees only
126        let layout_start = Instant::now();
127        if !self.dirty_nodes.is_empty() {
128            let screen_size = self.platform.screen_size();
129            self.layout.compute(
130                &self.tree,
131                screen_size.width,
132                screen_size.height,
133                &*self.platform,
134            )?;
135        }
136        let layout_duration = layout_start.elapsed();
137
138        // Phase 3: Mount — apply changes to native views
139        let mount_start = Instant::now();
140        let dirty_vec: Vec<_> = self.dirty_nodes.drain().collect();
141        self.mount_changes(&dirty_vec)?;
142        let mount_duration = mount_start.elapsed();
143
144        // Record frame stats for DevTools
145        self.scheduler.record_frame(
146            layout_duration,
147            mount_duration,
148            1, // batches processed
149        );
150
151        Ok(())
152    }
153
154    /// Enqueue a batch via the scheduler (called from JS via JSI).
155    /// The scheduler handles priority ordering and frame coalescing.
156    pub fn enqueue_commit(
157        &self,
158        batch: ir::IrBatch,
159        priority: scheduler::Priority,
160    ) {
161        self.scheduler.enqueue(batch, priority);
162    }
163
164    /// Process all pending scheduled work for this frame.
165    /// Called by the platform's vsync/display-link callback.
166    pub fn process_frame(&mut self) -> Result<(), EngineError> {
167        let batches = self.scheduler.drain_frame();
168        for batch in &batches {
169            self.apply_commit(batch)?;
170        }
171        Ok(())
172    }
173
174    /// Handle a navigation action.
175    pub fn navigate(
176        &mut self,
177        action: navigation::NavigationAction,
178    ) -> Vec<navigation::NavigationEvent> {
179        self.navigator.dispatch(action)
180    }
181
182    /// Get the navigator (for DevTools inspection).
183    pub fn navigator(&self) -> &Navigator {
184        &self.navigator
185    }
186
187    /// Get the focus manager.
188    pub fn focus_manager(&mut self) -> &mut FocusManager {
189        &mut self.focus
190    }
191
192    /// Get scheduler stats (for DevTools).
193    pub fn scheduler_stats(&self) -> scheduler::FrameStats {
194        self.scheduler.stats()
195    }
196
197    /// Apply IR commands to the shadow tree. Returns IDs of nodes that changed.
198    fn apply_ir_to_tree(
199        &mut self,
200        batch: &ir::IrBatch,
201    ) -> Result<Vec<tree::NodeId>, EngineError> {
202        let mut dirty = Vec::new();
203
204        for cmd in &batch.commands {
205            match cmd {
206                ir::IrCommand::CreateNode { id, view_type, props, style } => {
207                    self.tree.create_node(*id, view_type.clone(), props.clone());
208                    self.layout.create_node(*id, style)?;
209                    dirty.push(*id);
210                }
211                ir::IrCommand::UpdateProps { id, diff } => {
212                    self.tree.update_props(*id, diff);
213                    dirty.push(*id);
214                }
215                ir::IrCommand::UpdateStyle { id, style } => {
216                    self.layout.update_style(*id, style)?;
217                    dirty.push(*id);
218                }
219                ir::IrCommand::AppendChild { parent, child } => {
220                    self.tree.append_child(*parent, *child);
221                    self.layout.set_children_from_tree(*parent, &self.tree)?;
222                    dirty.push(*parent);
223                }
224                ir::IrCommand::InsertBefore { parent, child, before } => {
225                    self.tree.insert_before(*parent, *child, *before);
226                    self.layout.set_children_from_tree(*parent, &self.tree)?;
227                    dirty.push(*parent);
228                }
229                ir::IrCommand::RemoveChild { parent, child } => {
230                    self.tree.remove_child(*parent, *child);
231                    self.layout.remove_node(*child);
232                    dirty.push(*parent);
233                    dirty.push(*child);
234                }
235                ir::IrCommand::SetRootNode { id } => {
236                    self.tree.set_root(*id);
237                    self.layout.set_root(*id);
238                    dirty.push(*id);
239                }
240            }
241        }
242
243        Ok(dirty)
244    }
245
246    /// Mount phase: create/update/position native views.
247    fn mount_changes(
248        &mut self,
249        dirty_nodes: &[tree::NodeId],
250    ) -> Result<(), EngineError> {
251        for &node_id in dirty_nodes {
252            let (view_type, parent_info, native_handle) = match self.tree.get(node_id) {
253                Some(n) => (
254                    n.view_type.clone(),
255                    n.parent.and_then(|pid| {
256                        self.tree.get(pid).and_then(|p| {
257                            p.native_handle.map(|h| (h, p.children.iter().position(|&c| c == node_id).unwrap_or(0)))
258                        })
259                    }),
260                    n.native_handle,
261                ),
262                None => continue, // Node was removed
263            };
264
265            // Create native view if it doesn't exist yet
266            if native_handle.is_none() {
267                let handle = self.platform.create_view(view_type, node_id);
268                self.tree.set_native_handle(node_id, handle);
269
270                // If this node has a parent, insert into parent's native view
271                if let Some((parent_handle, index)) = parent_info {
272                    self.platform.insert_child(parent_handle, handle, index);
273                }
274            }
275
276            // Apply props to native view
277            if let Some(handle) = self.tree.get(node_id).and_then(|n| n.native_handle) {
278                let props_diff = self.tree.take_pending_props(node_id);
279                if !props_diff.is_empty() {
280                    self.platform.update_view(handle, &props_diff)
281                        .map_err(|e| EngineError::Platform(e.to_string()))?;
282                }
283
284                // Apply computed layout position
285                if let Some(layout) = self.layout.get_computed(node_id) {
286                    let mut position_props = platform::PropsDiff::new();
287                    position_props.set("frame", PropValue::Rect {
288                        x: layout.x,
289                        y: layout.y,
290                        width: layout.width,
291                        height: layout.height,
292                    });
293                    self.platform.update_view(handle, &position_props)
294                        .map_err(|e| EngineError::Platform(e.to_string()))?;
295                }
296            }
297        }
298
299        Ok(())
300    }
301
302    /// Handle a native input event.
303    /// Called by the platform bridge when the OS delivers touch/mouse/keyboard events.
304    pub fn handle_event(&mut self, event: InputEvent) -> events::EventResult {
305        self.events.dispatch(event, &self.layout, &self.tree)
306    }
307
308    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
309    // Sync path — read-only queries, takes &self (not &mut self)
310    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
311
312    /// Handle a synchronous call from JS.
313    /// This takes `&self` — compile-time guarantee that no mutation happens.
314    ///
315    /// RULE: No UI mutation allowed on the sync path.
316    pub fn handle_sync(&self, call: &SyncCall) -> SyncResult {
317        match call {
318            SyncCall::Measure { node_id } => {
319                match self.layout.get_computed(*node_id) {
320                    Some(layout) => SyncResult::from_layout(layout),
321                    None => SyncResult::NotFound,
322                }
323            }
324
325            SyncCall::IsFocused { node_id } => {
326                SyncResult::Bool {
327                    value: self.focus.focused() == Some(*node_id),
328                }
329            }
330
331            SyncCall::GetScrollOffset { node_id } => {
332                // Scroll offset is tracked per-node; placeholder until
333                // ScrollView tracking is wired up in the event system.
334                if self.tree.get(*node_id).is_some() {
335                    SyncResult::ScrollOffset { x: 0.0, y: 0.0 }
336                } else {
337                    SyncResult::NotFound
338                }
339            }
340
341            SyncCall::SupportsCapability { capability } => {
342                let cap = match capability.as_str() {
343                    "haptics" => Some(platform::PlatformCapability::Haptics),
344                    "biometrics" => Some(platform::PlatformCapability::Biometrics),
345                    "menuBar" => Some(platform::PlatformCapability::MenuBar),
346                    "systemTray" => Some(platform::PlatformCapability::SystemTray),
347                    "multiWindow" => Some(platform::PlatformCapability::MultiWindow),
348                    "dragAndDrop" => Some(platform::PlatformCapability::DragAndDrop),
349                    "contextMenu" => Some(platform::PlatformCapability::ContextMenu),
350                    "nativeShare" => Some(platform::PlatformCapability::NativeShare),
351                    "pushNotifications" => Some(platform::PlatformCapability::PushNotifications),
352                    "backgroundFetch" => Some(platform::PlatformCapability::BackgroundFetch),
353                    "nativeDatePicker" => Some(platform::PlatformCapability::NativeDatePicker),
354                    "nativeFilePicker" => Some(platform::PlatformCapability::NativeFilePicker),
355                    _ => None,
356                };
357                SyncResult::Bool {
358                    value: cap.map_or(false, |c| self.platform.supports(c)),
359                }
360            }
361
362            SyncCall::GetScreenInfo => {
363                let size = self.platform.screen_size();
364                SyncResult::ScreenInfo {
365                    width: size.width,
366                    height: size.height,
367                    scale: self.platform.scale_factor(),
368                }
369            }
370
371            SyncCall::IsProcessing => {
372                SyncResult::Bool {
373                    value: self.scheduler.is_processing(),
374                }
375            }
376
377            SyncCall::GetAccessibilityRole { node_id } => {
378                // Accessibility role lookup from shadow tree node props.
379                // Full a11y info will be richer once AccessibilityInfo is
380                // stored per-node; for now return based on view type.
381                match self.tree.get(*node_id) {
382                    Some(node) => {
383                        let role = match &node.view_type {
384                            platform::ViewType::Button => "button",
385                            platform::ViewType::Text => "text",
386                            platform::ViewType::TextInput => "textField",
387                            platform::ViewType::Image => "image",
388                            platform::ViewType::Switch => "switch",
389                            platform::ViewType::Slider => "adjustable",
390                            _ => "none",
391                        };
392                        SyncResult::Role { role: role.to_string() }
393                    }
394                    None => SyncResult::NotFound,
395                }
396            }
397
398            SyncCall::GetFrameStats => {
399                let stats = self.scheduler.stats();
400                SyncResult::FrameStats {
401                    frame_count: stats.frame_count,
402                    frames_dropped: stats.frames_dropped,
403                    last_frame_ms: stats.last_frame_duration.as_secs_f64() * 1000.0,
404                    last_layout_ms: stats.last_layout_duration.as_secs_f64() * 1000.0,
405                    last_mount_ms: stats.last_mount_duration.as_secs_f64() * 1000.0,
406                }
407            }
408
409            SyncCall::NodeExists { node_id } => {
410                SyncResult::Bool {
411                    value: self.tree.get(*node_id).is_some(),
412                }
413            }
414
415            SyncCall::GetChildCount { node_id } => {
416                match self.tree.get(*node_id) {
417                    Some(node) => SyncResult::Int { value: node.children.len() as u64 },
418                    None => SyncResult::NotFound,
419                }
420            }
421
422            SyncCall::MeasureText { text, style, max_width } => {
423                let platform_style = style.to_platform_style();
424                let metrics = self.platform.measure_text(text, &platform_style, *max_width);
425                SyncResult::TextMetrics {
426                    width: metrics.width,
427                    height: metrics.height,
428                    baseline: metrics.baseline,
429                    line_count: metrics.line_count,
430                }
431            }
432
433            SyncCall::GetFocusedNode => {
434                SyncResult::NodeIdResult {
435                    node_id: self.focus.focused().map(|id| id.0),
436                }
437            }
438
439            SyncCall::CanGoBack => {
440                SyncResult::Bool {
441                    value: self.navigator.can_go_back(),
442                }
443            }
444
445            SyncCall::GetActiveRoute => {
446                match self.navigator.active_screen() {
447                    Some(screen) => SyncResult::ActiveRoute {
448                        route_name: Some(screen.route_name.clone()),
449                        params: screen.params.clone(),
450                    },
451                    None => SyncResult::ActiveRoute {
452                        route_name: None,
453                        params: std::collections::HashMap::new(),
454                    },
455                }
456            }
457        }
458    }
459
460    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
461    // Async path — mutations, enqueued for next frame
462    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
463
464    /// Handle an asynchronous call from JS.
465    /// This takes `&mut self` — mutations are allowed.
466    pub fn handle_async(&mut self, call: AsyncCall) {
467        match &call {
468            AsyncCall::Navigate { .. } => {
469                if let Some(action) = call.to_navigation_action() {
470                    self.navigator.dispatch(action);
471                }
472            }
473            AsyncCall::SetFocus { node_id } => {
474                let _ = self.focus.focus(*node_id);
475            }
476            AsyncCall::MoveFocus { direction } => {
477                let _direction = direction; // TODO: directional focus traversal
478                // For now, just clear focus as placeholder
479            }
480            AsyncCall::Announce { message } => {
481                tracing::info!(message = %message, "a11y announcement");
482                // Platform-specific announcement will be routed through
483                // the platform bridge once the announce() method is added.
484            }
485        }
486    }
487
488    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
489    // Accessors (DevTools)
490    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
491
492    /// Get the shadow tree (for DevTools inspection).
493    pub fn tree(&self) -> &ShadowTree {
494        &self.tree
495    }
496
497    /// Get the layout engine (for DevTools layout overlay).
498    pub fn layout(&self) -> &LayoutEngine {
499        &self.layout
500    }
501}
502
503use platform::PropValue;
504
505#[derive(Debug, thiserror::Error)]
506pub enum EngineError {
507    #[error("Layout error: {0}")]
508    Layout(#[from] layout::LayoutError),
509
510    #[error("Platform error: {0}")]
511    Platform(String),
512
513    #[error("IR decode error: {0}")]
514    IrDecode(String),
515
516    #[error("Node not found: {0:?}")]
517    NodeNotFound(tree::NodeId),
518}