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