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