Skip to main content

pax_runtime/
api.rs

1use std::{
2    collections::HashMap,
3    rc::{Rc, Weak},
4};
5
6use_RefCell!();
7use crate::{
8    node_interface::NodeLocal, ExpandedNode, LayoutHull, RuntimeContext,
9    RuntimePropertiesStackFrame, TransformAndBounds,
10};
11use kurbo::Affine;
12
13pub use pax_runtime_api::*;
14use pax_runtime_api::{cursor::CursorStyle, math::Point2, properties::UntypedProperty};
15
16use crate::node_interface::NodeInterface;
17#[cfg(feature = "designtime")]
18use {
19    pax_designtime::{
20        messages::{UserlandSourceUpdateRequest, UserlandSourceUpdateResponse},
21        DesigntimeManager,
22    },
23    pax_manifest::UniqueTemplateNodeIdentifier,
24};
25
26#[derive(Clone)]
27/// Runtime context passed into user component lifecycle methods and event handlers.
28///
29/// Child-related fields intentionally separate semantic payload from engine
30/// transport:
31///
32/// - `projected_children` is the raw transport family used by `Slot`
33/// - `received_children` is the normalized semantic payload that this node
34///   should treat as content from its caller
35/// - `retained_received_children` are former received children kept alive only
36///   so `@out` transitions can finish
37///
38/// A node's own private template or primitive-assembled structure is
39/// intentionally not surfaced here as a first-class "child family" for
40/// container consumers.
41pub struct NodeContext {
42    pub expanded_node: Weak<ExpandedNode>,
43    /// slot index of this node in its container
44    pub slot_index: Property<Option<usize>>,
45    /// Stack frame of this component, used to look up stores
46    pub local_stack_frame: Rc<RuntimePropertiesStackFrame>,
47    /// Reference to the ExpandedNode of the component containing this node
48    pub containing_component: Weak<ExpandedNode>,
49    /// The current global engine frame count.
50    pub elapsed_frames: Property<u64>,
51    /// The current global engine wall-clock time in milliseconds.
52    pub elapsed_millis: Property<u64>,
53    /// Current device orientation sensor reading.
54    pub gyro: Property<Gyro>,
55    /// Current device accelerometer reading.
56    pub accel: Property<Accel>,
57    /// The bounds of this element's immediate container (parent) in px
58    pub bounds_parent: Property<(f64, f64)>,
59    /// The bounds of this element in px
60    pub bounds_self: Property<(f64, f64)>,
61    /// Measured bounds resolved by the chassis or container layout for this node.
62    pub measured_size: Property<Option<(f64, f64)>>,
63    /// Node-local subtree layout hull published by the engine for container measurement.
64    pub subtree_layout_hull: Property<LayoutHull>,
65    /// Current platform (Web/Native) this app is running on
66    pub platform: Platform,
67    /// Current os (Android/Windows/Mac/Linux) this app is running on
68    pub os: OS,
69    /// Derived target facts for platform/OS checks.
70    pub target: Property<TargetInfo>,
71    /// Derived viewport facts for size and orientation checks.
72    pub viewport: Property<Viewport>,
73    /// The number of projected children available to this node.
74    ///
75    /// This is the raw transport count used by slot-driven implementations.
76    /// Container-style consumers usually want `received_children_count`
77    /// instead.
78    pub projected_children_count: Property<usize>,
79    /// Borrow of the RuntimeContext, used at least for exposing raycasting to userland
80    pub(crate) runtime_context: Rc<RuntimeContext>,
81    /// The transform of this node in the global coordinate space
82    pub node_transform_and_bounds: TransformAndBounds<NodeLocal, Window>,
83    /// Children projected into this node from the containing component.
84    ///
85    /// Projection is an engine transport mechanism. Consumers that want the
86    /// semantic payload owned by this node should prefer `received_children`.
87    pub projected_children: Property<Vec<Rc<ExpandedNode>>>,
88    /// A structural invalidation signal for projected children.
89    pub projected_children_changed: Property<()>,
90    /// Semantic payload children received by this node from its caller.
91    ///
92    /// This is the canonical "content" view for container-style logic. It
93    /// excludes private encapsulated implementation children and also excludes
94    /// exit-retained payload nodes, which instead appear in
95    /// `retained_received_children`.
96    pub received_children: Property<Vec<Rc<ExpandedNode>>>,
97    /// Convenience count derived from `received_children`.
98    pub received_children_count: Property<usize>,
99    /// A structural invalidation signal for `received_children`.
100    ///
101    /// Prefer this or `received_children` itself for structural subscriptions
102    /// that must react to reorders as well as insertions and removals.
103    pub received_children_changed: Property<()>,
104    /// Received children retained only so exit transitions can finish.
105    ///
106    /// These are no longer part of the active semantic payload, but some
107    /// containers still need to place them as ghosts or overlays while their
108    /// `@out` transitions run.
109    pub retained_received_children: Property<Vec<Rc<ExpandedNode>>>,
110    /// A structural invalidation signal for `retained_received_children`.
111    pub retained_received_children_changed: Property<()>,
112
113    #[cfg(feature = "designtime")]
114    pub designtime: Rc<RefCell<DesigntimeManager>>,
115    pub(crate) get_elapsed_millis: Rc<dyn Fn() -> u128>,
116}
117
118impl NodeContext {
119    /// Push component-local state onto the runtime stack for descendants to find.
120    pub fn push_local_store<T: Store>(&self, store: T) {
121        self.local_stack_frame.insert_stack_local_store(store);
122    }
123
124    /// Borrow the nearest stack-local store of type `T`.
125    pub fn peek_local_store<T: Store, V>(&self, f: impl FnOnce(&mut T) -> V) -> Result<V, String> {
126        self.local_stack_frame.peek_stack_local_store(f)
127    }
128
129    /// Convert a window-space point into this node's local coordinate space, including any
130    /// presentation offsets inherited from ancestor scrollers.
131    pub fn local_point(&self, p: Point2<Window>) -> Point2<NodeLocal> {
132        let presentation_transform = self
133            .expanded_node
134            .upgrade()
135            .map(|node| {
136                self.runtime_context
137                    .presentation_scroll_transform_for_node(&node)
138            })
139            .unwrap_or(Affine::IDENTITY);
140        local_point_with_presentation_transform(
141            self.node_transform_and_bounds,
142            presentation_transform,
143            p,
144        )
145    }
146
147    /// Return the interface for this node's containing component, when present.
148    pub fn get_node_interface(&self) -> Option<NodeInterface> {
149        Weak::upgrade(&self.containing_component).map(|v| v.into())
150    }
151
152    /// Milliseconds elapsed according to the chassis-provided clock.
153    pub fn elapsed_time_millis(&self) -> u128 {
154        (self.get_elapsed_millis)()
155    }
156
157    /// Attach a dependency subscription whose callback runs when any dependency dirties.
158    pub fn subscribe(&self, dependencies: &[UntypedProperty], f: impl Fn() + 'static) {
159        match self.expanded_node.upgrade() {
160            Some(expanded_node) => {
161                let subscription_prop =
162                    self.runtime_context
163                        .register_node_effect(expanded_node.id, dependencies, f);
164                borrow_mut!(expanded_node.subscriptions).push(subscription_prop);
165            }
166            None => log::warn!("couldn't add subscription: node doesn't exist anymore"),
167        }
168    }
169
170    /// Remove all subscriptions registered on this node.
171    pub fn clear_subscriptions(&self) {
172        match self.expanded_node.upgrade() {
173            Some(expanded_node) => borrow_mut!(expanded_node.subscriptions).clear(),
174            None => log::warn!("couldn't clear subscriptions: node doesn't exist anymore"),
175        }
176    }
177
178    /// Ask the chassis to navigate to a URL.
179    ///
180    /// On web targets, same-origin navigation in the current tab can be handled
181    /// through the browser History API and routed back into Pax without a full
182    /// page reload. Other targets use the active chassis navigation behavior.
183    pub fn navigate_to(&self, url: &str, target: NavigationTarget) {
184        self.runtime_context
185            .enqueue_native_message(NativeMessage::Navigate(NavigationPatch {
186                url: url.to_string(),
187                target: match target {
188                    NavigationTarget::Current => "current",
189                    NavigationTarget::New => "new",
190                }
191                .to_string(),
192            }))
193    }
194
195    /// Queue a named custom event from this component for dispatch at the end of the tick.
196    pub fn dispatch_event(&self, identifier: &'static str) -> Result<(), String> {
197        let component_origin = self
198            .containing_component
199            .upgrade()
200            .ok_or_else(|| "can't dispatch from root component".to_owned())?;
201
202        // Check that this is a valid custom event to trigger
203        {
204            let component_origin_instance = borrow!(component_origin.instance_node);
205            let registry = component_origin_instance
206                .base()
207                .handler_registry
208                .as_ref()
209                .ok_or_else(|| "no registry present".to_owned())?;
210            borrow!(registry).handlers.get(identifier).ok_or_else(|| {
211                format!("no registered handler with name \"{}\" exists", identifier)
212            })?;
213        }
214
215        // ok now we know it's a valid thing to dispatch, queue it for end of tick
216        self.runtime_context
217            .queue_custom_event(Rc::clone(&component_origin), identifier);
218
219        Ok(())
220    }
221
222    /// Ask the chassis to display the requested cursor over the app surface.
223    pub fn set_cursor(&self, cursor: CursorStyle) {
224        self.runtime_context
225            .enqueue_native_message(NativeMessage::SetCursor(SetCursorPatch {
226                cursor: cursor.to_string(),
227            }));
228    }
229
230    /// Request a screenshot capture from the chassis, keyed by caller-provided id.
231    pub fn screenshot(&self, id: u32) {
232        self.runtime_context
233            .enqueue_native_message(NativeMessage::Screenshot(ScreenshotPatch {
234                id,
235                scale: None,
236            }));
237    }
238
239    /// Shared map where completed screenshot captures are published by id.
240    pub fn get_screenshot_map(&self) -> Rc<RefCell<HashMap<u32, ScreenshotData>>> {
241        self.runtime_context.get_screenshot_map()
242    }
243}
244
245fn local_point_with_presentation_transform(
246    node_transform_and_bounds: TransformAndBounds<NodeLocal, Window>,
247    presentation_transform: Affine,
248    point: Point2<Window>,
249) -> Point2<NodeLocal> {
250    node_transform_and_bounds.as_transform().inverse() * (presentation_transform.inverse() * point)
251}
252
253#[cfg(test)]
254mod local_point_tests {
255    use super::*;
256    use pax_runtime_api::math::{Transform2, Vector2};
257
258    #[test]
259    fn local_point_accounts_for_ancestor_scroller_presentation_offset() {
260        let node = TransformAndBounds {
261            transform: Transform2::<NodeLocal, Window>::translate(Vector2::new(50.0, 400.0)),
262            bounds: (200.0, 100.0),
263        };
264        let presentation_transform = Affine::translate((0.0, -300.0));
265
266        let local = local_point_with_presentation_transform(
267            node,
268            presentation_transform,
269            Point2::new(150.0, 140.0),
270        );
271
272        assert!((local.x - 0.5).abs() < 1e-9);
273        assert!((local.y - 0.4).abs() < 1e-9);
274    }
275}
276
277#[cfg(feature = "designtime")]
278impl NodeContext {
279    /// Send updated project source to the design server for designtime-only application.
280    pub fn submit_userland_source_update(
281        &self,
282        request_id: String,
283        path: String,
284        contents: String,
285    ) -> Result<(), String> {
286        self.designtime
287            .borrow_mut()
288            .send_userland_source_update(UserlandSourceUpdateRequest {
289                request_id,
290                path,
291                contents,
292            })
293            .map_err(|err| err.to_string())
294    }
295
296    /// Drain any queued responses from designtime source update requests.
297    pub fn take_userland_source_update_responses(&self) -> Vec<UserlandSourceUpdateResponse> {
298        self.designtime
299            .borrow_mut()
300            .take_userland_source_update_responses()
301    }
302
303    pub fn raycast(&self, point: Point2<Window>, hit_invisible: bool) -> Vec<NodeInterface> {
304        let expanded_nodes = self.runtime_context.get_elements_beneath_ray(
305            self.runtime_context.get_userland_root_expanded_node(),
306            point,
307            false,
308            vec![],
309            hit_invisible,
310        );
311        expanded_nodes
312            .into_iter()
313            .map(Into::<NodeInterface>::into)
314            .collect()
315    }
316
317    pub fn get_nodes_by_global_id(&self, uni: UniqueTemplateNodeIdentifier) -> Vec<NodeInterface> {
318        let expanded_nodes = self.runtime_context.get_expanded_nodes_by_global_ids(&uni);
319        expanded_nodes
320            .into_iter()
321            .map(Into::<NodeInterface>::into)
322            .collect()
323    }
324
325    pub fn get_userland_root_expanded_node(&self) -> Option<NodeInterface> {
326        #[cfg(feature = "designtime")]
327        let expanded_node = self.runtime_context.get_userland_root_expanded_node()?;
328        #[cfg(not(feature = "designtime"))]
329        let expanded_node = self.runtime_context.get_root_expanded_node()?;
330        Some(expanded_node.into())
331    }
332
333    pub fn get_root_expanded_node(&self) -> Option<NodeInterface> {
334        let expanded_node = self.runtime_context.get_root_expanded_node()?;
335        Some(expanded_node.into())
336    }
337
338    pub fn get_nodes_by_id(&self, id: &str) -> Vec<NodeInterface> {
339        let expanded_nodes = self.runtime_context.get_expanded_nodes_by_id(id);
340        expanded_nodes
341            .into_iter()
342            .map(Into::<NodeInterface>::into)
343            .collect()
344    }
345}