use crate::api::math::Point2;
use crate::api::Window;
use crate::constants::{ACCEL_HANDLERS, GYRO_HANDLERS, PRE_RENDER_HANDLERS, TICK_HANDLERS};
use pax_language::interpreter::property_resolution::IdentifierResolver;
use pax_manifest::cartridge_generation::{TRANSITION_PHASE_ENTER, TRANSITION_PHASE_EXIT};
use pax_manifest::UniqueTemplateNodeIdentifier;
use pax_message::{NativeMessage, SafeAreaInsets, ScreenshotData};
use pax_runtime_api::properties::{
drain_effects, drain_effects_with_report, property_has_direct_outbound,
property_outbound_debug_names, register_effect_property, register_effect_property_with_name,
UntypedProperty,
};
use pax_runtime_api::{
borrow, borrow_mut, use_RefCell, Event, Interpolatable, LightShape, LocalProperty, MouseOut,
MouseOver, RenderContext, SceneLight, SceneLighting, Variable,
};
use_RefCell!();
use kurbo::{Affine, Point};
use std::cell::Cell;
use std::collections::{HashMap, HashSet};
use std::hash::Hash;
use std::rc::{Rc, Weak};
use std::sync::OnceLock;
use std::time::Instant;
use crate::{ExpandedNode, Globals};
#[cfg(feature = "designtime")]
use crate::{ComponentInstance, InstanceNode};
fn effect_drain_instrumentation_enabled() -> bool {
static ENABLED: OnceLock<bool> = OnceLock::new();
*ENABLED.get_or_init(|| {
std::env::var("PAX_EFFECT_DRAIN_INSTRUMENTATION")
.map(|value| {
let value = value.to_ascii_lowercase();
matches!(value.as_str(), "1" | "true" | "yes" | "on")
})
.unwrap_or(false)
})
}
fn format_effect_top(top_effects: &[(String, usize)]) -> String {
top_effects
.iter()
.map(|(name, count)| format!("{count}x {name}"))
.collect::<Vec<_>>()
.join("; ")
}
fn summarize_debug_names(names: Vec<String>) -> Vec<(String, usize)> {
let mut counts = HashMap::new();
for name in names {
let category = name
.split_once(" (")
.map(|(category, _)| category)
.unwrap_or(&name)
.to_owned();
*counts.entry(category).or_insert(0) += 1;
}
let mut counts = counts.into_iter().collect::<Vec<_>>();
counts.sort_by(|(left_name, left_count), (right_name, right_count)| {
right_count
.cmp(left_count)
.then_with(|| left_name.cmp(right_name))
});
counts
}
impl Interpolatable for ExpandedNodeIdentifier {}
fn transform_scene_light_for_layer(
mut light: SceneLight,
source_to_root: Affine,
root_to_target: Affine,
) -> SceneLight {
if matches!(light.shape, LightShape::Point) {
let point = Point::new(light.position.x, light.position.y);
let point = root_to_target * (source_to_root * point);
light.position.x = point.x;
light.position.y = point.y;
}
light
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct ExpandedNodeIdentifier(pub u32);
impl ExpandedNodeIdentifier {
pub fn to_u32(&self) -> u32 {
self.0
}
}
pub struct RuntimeContext {
pub application: pax_runtime_api::application::ApplicationInstance,
pub property_graph: pax_runtime_api::PropertyGraph,
next_uid: Cell<ExpandedNodeIdentifier>,
messages: RefCell<Vec<NativeMessage>>,
globals: RefCell<Globals>,
root_expanded_node: RefCell<Weak<ExpandedNode>>,
#[cfg(feature = "designtime")]
pub userland_frame_instance_node: RefCell<Option<Rc<dyn InstanceNode>>>,
#[cfg(feature = "designtime")]
pub userland_root_expanded_node: RefCell<Option<Rc<ExpandedNode>>>,
node_cache: RefCell<NodeCache>,
last_topmost_element: RefCell<Weak<ExpandedNode>>,
active_touch_targets: RefCell<HashMap<i64, ExpandedNodeIdentifier>>,
queued_custom_events: RefCell<Vec<(Rc<ExpandedNode>, &'static str)>>,
queued_renders: RefCell<Vec<Rc<ExpandedNode>>>,
pub layer_count: Cell<usize>,
pub dirty_canvases: Rc<RefCell<Vec<bool>>>,
dirty_canvas_nodes: RefCell<HashSet<ExpandedNodeIdentifier>>,
scene_geometry: RefCell<crate::scene_geometry::SceneGeometry>,
pub(crate) viewport_proximity: RefCell<crate::viewport_proximity::ViewportProximityState>,
canvas_node_light_masks: RefCell<HashMap<ExpandedNodeIdentifier, u32>>,
scene_lighting_nodes: RefCell<HashSet<ExpandedNodeIdentifier>>,
scene_lighting_dirty: Cell<bool>,
layer_light_scopes:
RefCell<HashMap<usize, Vec<(ExpandedNodeIdentifier, Option<ExpandedNodeIdentifier>)>>>,
#[cfg(test)]
lighting_mask_node_visits: Cell<usize>,
lighting_overflow_counts: RefCell<HashMap<usize, usize>>,
targeted_canvas_replay_node_ids: RefCell<HashMap<usize, HashSet<u32>>>,
removed_canvas_nodes: RefCell<Vec<(usize, u32)>>,
occlusion_dirty: Cell<bool>,
pub(crate) occlusion_state: RefCell<crate::engine::occlusion::OcclusionState>,
layer_canvas_plan_generation: Cell<u32>,
canvas_drawable_layers: RefCell<HashSet<usize>>,
import_settings_node_count: Cell<usize>,
tick_handler_nodes: RefCell<Vec<ExpandedNodeIdentifier>>,
pre_render_handler_nodes: RefCell<Vec<ExpandedNodeIdentifier>>,
gyro_handler_nodes: RefCell<Vec<ExpandedNodeIdentifier>>,
accel_handler_nodes: RefCell<Vec<ExpandedNodeIdentifier>>,
screenshot_map: Rc<RefCell<HashMap<u32, ScreenshotData>>>,
scroller_surface_states: RefCell<HashMap<u32, ScrollerSurfaceState>>,
layer_scroller_owners: RefCell<HashMap<usize, ExpandedNodeIdentifier>>,
root_scroller_id: Cell<Option<u32>>,
visual_viewport_state: Cell<Option<VisualViewportState>>,
safe_area_insets: LocalProperty<SafeAreaInsets>,
}
#[derive(Clone, Copy, Debug, Default)]
pub struct ScrollerSurfaceState {
pub viewport_width: f64,
pub viewport_height: f64,
pub content_width: f64,
pub content_height: f64,
pub scroll_x: f64,
pub scroll_y: f64,
pub presentation_scroll_x: f64,
pub presentation_scroll_y: f64,
pub clip_content: bool,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ScrollerSurfaceStateChange {
Unchanged,
ScrollOnly,
Structural,
}
impl ScrollerSurfaceState {
fn close_enough(lhs: f64, rhs: f64) -> bool {
(lhs - rhs).abs() <= 1e-4
}
fn structural_eq(&self, other: &Self) -> bool {
Self::close_enough(self.viewport_width, other.viewport_width)
&& Self::close_enough(self.viewport_height, other.viewport_height)
&& Self::close_enough(self.content_width, other.content_width)
&& Self::close_enough(self.content_height, other.content_height)
&& self.clip_content == other.clip_content
}
fn scroll_eq(&self, other: &Self) -> bool {
Self::close_enough(self.scroll_x, other.scroll_x)
&& Self::close_enough(self.scroll_y, other.scroll_y)
&& Self::close_enough(self.presentation_scroll_x, other.presentation_scroll_x)
&& Self::close_enough(self.presentation_scroll_y, other.presentation_scroll_y)
}
}
#[derive(Clone, Copy, Debug, Default)]
pub struct VisualViewportState {
pub width: f64,
pub height: f64,
pub offset_x: f64,
pub offset_y: f64,
pub page_scroll_x: f64,
pub page_scroll_y: f64,
}
struct NodeCache {
eid_to_node: HashMap<ExpandedNodeIdentifier, Rc<ExpandedNode>>,
uni_to_eid: HashMap<UniqueTemplateNodeIdentifier, Vec<ExpandedNodeIdentifier>>,
}
impl NodeCache {
fn new() -> Self {
Self {
eid_to_node: Default::default(),
uni_to_eid: Default::default(),
}
}
fn add_to_cache(&mut self, node: &Rc<ExpandedNode>) {
self.eid_to_node.insert(node.id, Rc::clone(&node));
let uni = borrow!(node.instance_node)
.base()
.template_node_identifier
.clone();
if let Some(uni) = uni {
self.uni_to_eid.entry(uni).or_default().push(node.id);
}
}
fn remove_from_cache(&mut self, node: &Rc<ExpandedNode>) {
self.eid_to_node.remove(&node.id);
if let Some(uni) = &borrow!(node.instance_node).base().template_node_identifier {
self.uni_to_eid
.entry(uni.clone())
.or_default()
.retain(|&n| n != node.id);
}
}
}
impl RuntimeContext {
#[cfg(not(feature = "designtime"))]
pub fn new(globals: Globals) -> Self {
let application =
pax_runtime_api::application::ApplicationInstance::for_runtime(globals.target);
Self {
property_graph: application.property_graph(),
application,
next_uid: Cell::new(ExpandedNodeIdentifier(0)),
messages: RefCell::new(Vec::new()),
globals: RefCell::new(globals),
root_expanded_node: RefCell::new(Weak::new()),
node_cache: RefCell::new(NodeCache::new()),
active_touch_targets: Default::default(),
queued_custom_events: Default::default(),
queued_renders: Default::default(),
layer_count: Cell::default(),
last_topmost_element: Default::default(),
dirty_canvases: Default::default(),
dirty_canvas_nodes: Default::default(),
scene_geometry: Default::default(),
viewport_proximity: Default::default(),
canvas_node_light_masks: Default::default(),
scene_lighting_nodes: Default::default(),
scene_lighting_dirty: Default::default(),
layer_light_scopes: Default::default(),
#[cfg(test)]
lighting_mask_node_visits: Cell::new(0),
lighting_overflow_counts: Default::default(),
targeted_canvas_replay_node_ids: Default::default(),
removed_canvas_nodes: Default::default(),
occlusion_dirty: Cell::new(true),
occlusion_state: Default::default(),
layer_canvas_plan_generation: Cell::new(1),
canvas_drawable_layers: Default::default(),
import_settings_node_count: Cell::new(0),
tick_handler_nodes: Default::default(),
pre_render_handler_nodes: Default::default(),
gyro_handler_nodes: Default::default(),
accel_handler_nodes: Default::default(),
screenshot_map: Default::default(),
scroller_surface_states: Default::default(),
layer_scroller_owners: Default::default(),
root_scroller_id: Cell::new(None),
visual_viewport_state: Cell::new(None),
safe_area_insets: LocalProperty::default(),
}
}
#[cfg(feature = "designtime")]
pub fn new(globals: Globals, userland: Rc<ComponentInstance>) -> Self {
let application =
pax_runtime_api::application::ApplicationInstance::for_runtime(globals.target);
Self {
property_graph: application.property_graph(),
application,
next_uid: Cell::new(ExpandedNodeIdentifier(0)),
messages: RefCell::new(Vec::new()),
globals: RefCell::new(globals),
root_expanded_node: RefCell::new(Weak::new()),
userland_frame_instance_node: RefCell::new(Some(userland)),
userland_root_expanded_node: Default::default(),
node_cache: RefCell::new(NodeCache::new()),
active_touch_targets: Default::default(),
queued_custom_events: Default::default(),
queued_renders: Default::default(),
layer_count: Cell::default(),
last_topmost_element: Default::default(),
dirty_canvases: Default::default(),
dirty_canvas_nodes: Default::default(),
scene_geometry: Default::default(),
viewport_proximity: Default::default(),
canvas_node_light_masks: Default::default(),
scene_lighting_nodes: Default::default(),
scene_lighting_dirty: Default::default(),
layer_light_scopes: Default::default(),
#[cfg(test)]
lighting_mask_node_visits: Cell::new(0),
lighting_overflow_counts: Default::default(),
targeted_canvas_replay_node_ids: Default::default(),
removed_canvas_nodes: Default::default(),
occlusion_dirty: Cell::new(true),
occlusion_state: Default::default(),
layer_canvas_plan_generation: Cell::new(1),
canvas_drawable_layers: Default::default(),
import_settings_node_count: Cell::new(0),
tick_handler_nodes: Default::default(),
pre_render_handler_nodes: Default::default(),
gyro_handler_nodes: Default::default(),
accel_handler_nodes: Default::default(),
screenshot_map: Default::default(),
scroller_surface_states: Default::default(),
layer_scroller_owners: Default::default(),
root_scroller_id: Cell::new(None),
visual_viewport_state: Cell::new(None),
safe_area_insets: LocalProperty::default(),
}
}
#[cfg(feature = "designtime")]
pub fn new_empty(globals: Globals) -> Self {
let application =
pax_runtime_api::application::ApplicationInstance::for_runtime(globals.target);
Self {
property_graph: application.property_graph(),
application,
next_uid: Cell::new(ExpandedNodeIdentifier(0)),
messages: RefCell::new(Vec::new()),
globals: RefCell::new(globals),
root_expanded_node: RefCell::new(Weak::new()),
userland_frame_instance_node: RefCell::new(None),
userland_root_expanded_node: Default::default(),
node_cache: RefCell::new(NodeCache::new()),
active_touch_targets: Default::default(),
queued_custom_events: Default::default(),
queued_renders: Default::default(),
layer_count: Cell::default(),
last_topmost_element: Default::default(),
dirty_canvases: Default::default(),
dirty_canvas_nodes: Default::default(),
scene_geometry: Default::default(),
viewport_proximity: Default::default(),
canvas_node_light_masks: Default::default(),
scene_lighting_nodes: Default::default(),
scene_lighting_dirty: Default::default(),
layer_light_scopes: Default::default(),
#[cfg(test)]
lighting_mask_node_visits: Cell::new(0),
lighting_overflow_counts: Default::default(),
targeted_canvas_replay_node_ids: Default::default(),
removed_canvas_nodes: Default::default(),
occlusion_dirty: Cell::new(true),
occlusion_state: Default::default(),
layer_canvas_plan_generation: Cell::new(1),
canvas_drawable_layers: Default::default(),
import_settings_node_count: Cell::new(0),
tick_handler_nodes: Default::default(),
pre_render_handler_nodes: Default::default(),
gyro_handler_nodes: Default::default(),
accel_handler_nodes: Default::default(),
screenshot_map: Default::default(),
scroller_surface_states: Default::default(),
layer_scroller_owners: Default::default(),
root_scroller_id: Cell::new(None),
visual_viewport_state: Cell::new(None),
safe_area_insets: LocalProperty::default(),
}
}
pub fn register_root_expanded_node(&self, root: &Rc<ExpandedNode>) {
*borrow_mut!(self.root_expanded_node) = Rc::downgrade(root);
}
pub fn clear_root_expanded_node(&self) {
*borrow_mut!(self.root_expanded_node) = Weak::new();
let mut state = self.occlusion_state.borrow_mut();
let stats = state.stats;
*state = Default::default();
state.stats = stats;
}
pub fn add_to_cache(&self, node: &Rc<ExpandedNode>) {
borrow_mut!(self.node_cache).add_to_cache(node);
self.refresh_scene_lighting_node(node);
self.register_node_lifecycle_handlers(node);
if borrow!(node.instance_node).base().flags().layer == crate::api::Layer::Canvas {
self.mark_canvas_node_dirty(node.id);
if !node.is_render_source() {
self.set_canvas_dirty(node.occlusion.get().render_layer_id);
}
}
if node.is_import_settings_node() {
self.import_settings_node_count
.set(self.import_settings_node_count.get() + 1);
}
self.mark_occlusion_dirty();
}
pub fn remove_from_cache(&self, node: &Rc<ExpandedNode>) {
borrow_mut!(self.node_cache).remove_from_cache(node);
if borrow_mut!(self.scene_lighting_nodes).remove(&node.id) {
self.set_all_canvases_dirty();
}
self.unregister_viewport_proximity(node.id.to_u32());
borrow_mut!(self.scene_geometry).remove(node.id.to_u32());
self.occlusion_state.borrow_mut().remove(node.id.to_u32());
borrow_mut!(self.canvas_node_light_masks).remove(&node.id);
borrow_mut!(self.active_touch_targets).retain(|_, target| *target != node.id);
self.unregister_node_lifecycle_handlers(node.id);
if node.is_import_settings_node() {
self.import_settings_node_count
.set(self.import_settings_node_count.get().saturating_sub(1));
}
self.mark_occlusion_dirty();
}
pub fn has_import_settings_nodes(&self) -> bool {
self.import_settings_node_count.get() > 0
}
pub(crate) fn refresh_scene_lighting_node(&self, node: &ExpandedNode) {
let mut contributors = borrow_mut!(self.scene_lighting_nodes);
let changed = if borrow!(node.instance_node).has_scene_lighting() {
contributors.insert(node.id)
} else {
contributors.remove(&node.id)
};
if changed {
self.set_all_canvases_dirty();
}
}
pub fn register_node_effect_property(
&self,
node: ExpandedNodeIdentifier,
prop: &LocalProperty<()>,
) {
self.register_node_effect_property_named(node, prop, "node effect");
}
pub fn register_node_effect_property_named(
&self,
node: ExpandedNodeIdentifier,
prop: &LocalProperty<()>,
effect_name: &str,
) {
if effect_drain_instrumentation_enabled() {
let debug_name = self.node_effect_debug_name(node, effect_name);
register_effect_property_with_name(prop, &debug_name);
} else {
register_effect_property(prop);
}
}
pub fn register_expanded_node_effect_property_named(
&self,
node: &Rc<ExpandedNode>,
prop: &LocalProperty<()>,
effect_name: &str,
) {
if effect_drain_instrumentation_enabled() {
let debug_name = Self::expanded_node_effect_debug_name(node, effect_name);
register_effect_property_with_name(prop, &debug_name);
} else {
register_effect_property(prop);
}
}
pub fn register_node_effect(
&self,
node: ExpandedNodeIdentifier,
dependencies: &[UntypedProperty],
effect: impl Fn() + 'static,
) -> LocalProperty<()> {
let prop = LocalProperty::computed(effect, dependencies);
self.register_node_effect_property_named(node, &prop, "registered node effect");
prop
}
pub fn drain_node_effects(&self) {
let _graph = self.property_graph.enter();
self.property_graph.import();
const MAX_NODE_EFFECTS_PER_TICK: usize = 100_000;
if !effect_drain_instrumentation_enabled() {
let drained = drain_effects(MAX_NODE_EFFECTS_PER_TICK);
if drained == MAX_NODE_EFFECTS_PER_TICK {
log::warn!(
"reactive drain hit {} evaluations in one tick; deferring remaining work",
MAX_NODE_EFFECTS_PER_TICK
);
}
return;
}
let start = Instant::now();
let report = drain_effects_with_report(MAX_NODE_EFFECTS_PER_TICK);
let elapsed_ms = start.elapsed().as_secs_f64() * 1000.0;
if report.budget_exhausted {
log::warn!(
"reactive drain hit {} evaluations in one tick; deferring remaining work",
MAX_NODE_EFFECTS_PER_TICK
);
}
if report.popped > 0 || report.cutoffs_evaluated > 0 {
let globals = self.globals();
let frame_dependents = summarize_debug_names(property_outbound_debug_names(
&globals.elapsed_frames.untyped(),
));
let millis_dependents = summarize_debug_names(property_outbound_debug_names(
&globals.elapsed_millis.untyped(),
));
let frame_clock = globals.elapsed_frames.untyped();
let millis_clock = globals.elapsed_millis.untyped();
let (entering, exiting, enter_cleanup, exit_cleanup, stale_clock_nodes) = {
let nodes = borrow!(self.node_cache);
nodes.eid_to_node.values().fold(
(0, 0, 0, 0, Vec::new()),
|(entering, exiting, enter_cleanup, exit_cleanup, mut stale_clock_nodes),
node| {
let phase = node.transition_phase.get();
if phase != TRANSITION_PHASE_ENTER
&& phase != TRANSITION_PHASE_EXIT
&& (property_has_direct_outbound(
&frame_clock,
&node.transition_playhead.untyped(),
) || property_has_direct_outbound(
&millis_clock,
&node.transition_playhead_millis.untyped(),
))
{
stale_clock_nodes
.push(Self::expanded_node_effect_debug_name(node, "idle clock"));
}
(
entering + usize::from(phase == TRANSITION_PHASE_ENTER),
exiting + usize::from(phase == TRANSITION_PHASE_EXIT),
enter_cleanup + usize::from(node.enter_cleanup_active.get()),
exit_cleanup + usize::from(node.exit_cleanup_active.get()),
stale_clock_nodes,
)
},
)
};
println!(
"[PaxEffectDrain] ran={} popped={} clean={} unregistered={} missing={} remaining={} cutoffs={} suppressed={} propagated={} remaining_cutoffs={} elapsed_ms={:.3} transitions=enter:{}/exit:{}/enter_cleanup:{}/exit_cleanup:{} stale_clocks=[{}] frame_deps=[{}] millis_deps=[{}] effects=[{}] cutoff_nodes=[{}]",
report.ran,
report.popped,
report.skipped_clean,
report.skipped_unregistered,
report.skipped_missing,
report.remaining,
report.cutoffs_evaluated,
report.cutoffs_suppressed,
report.cutoffs_propagated,
report.remaining_cutoffs,
elapsed_ms,
entering,
exiting,
enter_cleanup,
exit_cleanup,
stale_clock_nodes.join("; "),
format_effect_top(&frame_dependents),
format_effect_top(&millis_dependents),
format_effect_top(&report.top_effects),
format_effect_top(&report.top_cutoffs),
);
}
}
fn node_effect_debug_name(&self, node: ExpandedNodeIdentifier, effect_name: &str) -> String {
let node = borrow!(self.node_cache).eid_to_node.get(&node).cloned();
if let Some(node) = node {
Self::expanded_node_effect_debug_name(&node, effect_name)
} else {
format!("{effect_name} node={node:?}")
}
}
fn expanded_node_effect_debug_name(node: &Rc<ExpandedNode>, effect_name: &str) -> String {
let instance_node = borrow!(node.instance_node);
let base = instance_node.base();
let type_name = base
.template_node_type_id
.as_ref()
.map(|type_id| {
type_id
.get_pascal_identifier()
.unwrap_or_else(|| type_id.to_string())
})
.unwrap_or_else(|| "<unknown>".to_owned());
format!("{effect_name} type={type_name}")
}
pub fn tick_handler_nodes(&self) -> Vec<ExpandedNodeIdentifier> {
borrow!(self.tick_handler_nodes).clone()
}
pub fn pre_render_handler_nodes(&self) -> Vec<ExpandedNodeIdentifier> {
borrow!(self.pre_render_handler_nodes).clone()
}
pub fn gyro_handler_nodes(&self) -> Vec<ExpandedNodeIdentifier> {
borrow!(self.gyro_handler_nodes).clone()
}
pub fn accel_handler_nodes(&self) -> Vec<ExpandedNodeIdentifier> {
borrow!(self.accel_handler_nodes).clone()
}
fn register_node_lifecycle_handlers(&self, node: &Rc<ExpandedNode>) {
let registry = borrow!(node.instance_node)
.base()
.handler_registry
.as_ref()
.map(Rc::clone);
let Some(registry) = registry else {
return;
};
let handlers = &borrow!(registry).handlers;
if handlers.get(TICK_HANDLERS).is_some_and(|h| !h.is_empty()) {
self.register_lifecycle_handler(&self.tick_handler_nodes, node.id);
}
if handlers
.get(PRE_RENDER_HANDLERS)
.is_some_and(|h| !h.is_empty())
{
self.register_lifecycle_handler(&self.pre_render_handler_nodes, node.id);
}
if handlers.get(GYRO_HANDLERS).is_some_and(|h| !h.is_empty()) {
self.register_lifecycle_handler(&self.gyro_handler_nodes, node.id);
}
if handlers.get(ACCEL_HANDLERS).is_some_and(|h| !h.is_empty()) {
self.register_lifecycle_handler(&self.accel_handler_nodes, node.id);
}
}
fn unregister_node_lifecycle_handlers(&self, id: ExpandedNodeIdentifier) {
borrow_mut!(self.tick_handler_nodes).retain(|node_id| *node_id != id);
borrow_mut!(self.pre_render_handler_nodes).retain(|node_id| *node_id != id);
borrow_mut!(self.gyro_handler_nodes).retain(|node_id| *node_id != id);
borrow_mut!(self.accel_handler_nodes).retain(|node_id| *node_id != id);
}
fn register_lifecycle_handler(
&self,
handler_nodes: &RefCell<Vec<ExpandedNodeIdentifier>>,
id: ExpandedNodeIdentifier,
) {
let mut handler_nodes = borrow_mut!(handler_nodes);
if !handler_nodes.contains(&id) {
handler_nodes.push(id);
}
}
pub fn get_expanded_node_by_eid(&self, id: ExpandedNodeIdentifier) -> Option<Rc<ExpandedNode>> {
borrow!(self.node_cache).eid_to_node.get(&id).cloned()
}
pub fn capture_touch_target(&self, identifier: i64, target: ExpandedNodeIdentifier) {
borrow_mut!(self.active_touch_targets).insert(identifier, target);
}
pub fn captured_touch_target(&self, identifier: i64) -> Option<Rc<ExpandedNode>> {
let target = borrow!(self.active_touch_targets)
.get(&identifier)
.copied()?;
self.get_expanded_node_by_eid(target)
}
pub fn release_touch_target(&self, identifier: i64) -> Option<Rc<ExpandedNode>> {
let target = borrow_mut!(self.active_touch_targets).remove(&identifier)?;
self.get_expanded_node_by_eid(target)
}
pub fn load_screenshot(&self, id: u32, data: ScreenshotData) -> bool {
borrow_mut!(self.screenshot_map).insert(id, data);
true
}
pub fn get_screenshot_map(&self) -> Rc<RefCell<HashMap<u32, ScreenshotData>>> {
Rc::clone(&self.screenshot_map)
}
pub fn set_scroller_surface_state(
&self,
id: u32,
state: ScrollerSurfaceState,
) -> ScrollerSurfaceStateChange {
let change = {
let mut states = borrow_mut!(self.scroller_surface_states);
let change = match states.get(&id) {
Some(previous) if previous.structural_eq(&state) && previous.scroll_eq(&state) => {
ScrollerSurfaceStateChange::Unchanged
}
Some(previous) if previous.structural_eq(&state) => {
ScrollerSurfaceStateChange::ScrollOnly
}
_ => ScrollerSurfaceStateChange::Structural,
};
if change != ScrollerSurfaceStateChange::Unchanged {
states.insert(id, state);
}
change
};
match change {
ScrollerSurfaceStateChange::Unchanged => {}
ScrollerSurfaceStateChange::ScrollOnly => {
self.mark_occlusion_presentation_dirty(id);
self.invalidate_viewport_presentation();
self.mark_layer_canvas_plans_dirty();
self.mark_scroller_content_layers_dirty(id);
}
ScrollerSurfaceStateChange::Structural => {
self.invalidate_viewport_presentation();
self.mark_layer_canvas_plans_dirty();
self.mark_occlusion_dirty();
}
}
change
}
pub fn update_scroller_surface_scroll(
&self,
id: u32,
scroll_x: f64,
scroll_y: f64,
presentation_scroll_x: f64,
presentation_scroll_y: f64,
) -> ScrollerSurfaceStateChange {
let change = {
let mut states = borrow_mut!(self.scroller_surface_states);
let Some(state) = states.get_mut(&id) else {
return ScrollerSurfaceStateChange::Unchanged;
};
let next = ScrollerSurfaceState {
scroll_x,
scroll_y,
presentation_scroll_x,
presentation_scroll_y,
..*state
};
let change = if state.scroll_eq(&next) {
ScrollerSurfaceStateChange::Unchanged
} else {
ScrollerSurfaceStateChange::ScrollOnly
};
if change != ScrollerSurfaceStateChange::Unchanged {
*state = next;
}
change
};
if change == ScrollerSurfaceStateChange::ScrollOnly {
self.mark_occlusion_presentation_dirty(id);
self.invalidate_viewport_presentation();
self.mark_layer_canvas_plans_dirty();
}
change
}
pub fn remove_scroller_surface_state(&self, id: u32) {
borrow_mut!(self.scroller_surface_states).remove(&id);
self.invalidate_viewport_presentation();
self.mark_layer_canvas_plans_dirty();
self.mark_occlusion_dirty();
}
pub fn get_scroller_surface_state(&self, id: u32) -> Option<ScrollerSurfaceState> {
borrow!(self.scroller_surface_states).get(&id).cloned()
}
pub fn get_scroller_surface_scroll(&self, id: u32) -> Option<(f64, f64)> {
borrow!(self.scroller_surface_states).get(&id).map(|state| {
let scroll_x = if state.presentation_scroll_x.is_finite() {
state.presentation_scroll_x
} else {
state.scroll_x
};
let scroll_y = if state.presentation_scroll_y.is_finite() {
state.presentation_scroll_y
} else {
state.scroll_y
};
(scroll_x, scroll_y)
})
}
pub(crate) fn scroller_content_presentation_transform(&self, node: &ExpandedNode) -> Affine {
fn clamp_offset(value: f64, content: f64, viewport: f64) -> f64 {
if content <= viewport || !value.is_finite() {
return 0.0;
}
value.max(0.0).min((content - viewport).max(0.0))
}
let root_delegates_to_page_scroll = self.get_root_scroller_id() == Some(node.id.to_u32())
&& self.get_visual_viewport_state().is_some();
if root_delegates_to_page_scroll {
return Affine::IDENTITY;
}
let instance_node = borrow!(node.instance_node);
let (scroll_x, scroll_y) = if self.get_root_scroller_id() == Some(node.id.to_u32()) {
if let Some(visual) = self.get_visual_viewport_state() {
let visual_x = visual.page_scroll_x + visual.offset_x;
let visual_y = visual.page_scroll_y + visual.offset_y;
if visual_x.is_finite() && visual_y.is_finite() {
if let Some(state) = self.get_scroller_surface_state(node.id.to_u32()) {
let viewport_width = if visual.width.is_finite() {
visual.width
} else {
state.viewport_width
};
let viewport_height = if visual.height.is_finite() {
visual.height
} else {
state.viewport_height
};
(
clamp_offset(visual_x, state.content_width, viewport_width),
clamp_offset(visual_y, state.content_height, viewport_height),
)
} else {
(visual_x, visual_y)
}
} else {
self.get_scroller_surface_scroll(node.id.to_u32())
.or_else(|| instance_node.resolve_scroll_offset(node))
.unwrap_or((0.0, 0.0))
}
} else {
self.get_scroller_surface_scroll(node.id.to_u32())
.or_else(|| instance_node.resolve_scroll_offset(node))
.unwrap_or((0.0, 0.0))
}
} else {
self.get_scroller_surface_scroll(node.id.to_u32())
.or_else(|| instance_node.resolve_scroll_offset(node))
.unwrap_or((0.0, 0.0))
};
if scroll_x.abs() <= f64::EPSILON && scroll_y.abs() <= f64::EPSILON {
return Affine::IDENTITY;
}
let world_transform = Affine::from(node.transform_and_bounds.get().transform);
let inverse_world = Affine::from(node.transform_and_bounds.get().transform.inverse());
world_transform * Affine::translate((-scroll_x, -scroll_y)) * inverse_world
}
pub(crate) fn presentation_scroll_transform_for_node(&self, node: &ExpandedNode) -> Affine {
let mut ancestors = Vec::new();
let mut current = node.render_parent_node();
while let Some(parent) = current {
current = parent.render_parent_node();
ancestors.push(parent);
}
ancestors.reverse();
ancestors
.into_iter()
.filter(|ancestor| borrow!(ancestor.instance_node).scrolls_content(ancestor))
.fold(Affine::IDENTITY, |transform, scroller| {
transform * self.scroller_content_presentation_transform(&scroller)
})
}
pub fn clear_layer_scroller_owners(&self) {
borrow_mut!(self.layer_scroller_owners).clear();
self.mark_layer_canvas_plans_dirty();
}
pub fn register_layer_scroller_owner(
&self,
layer_id: usize,
scroller_id: ExpandedNodeIdentifier,
) {
borrow_mut!(self.layer_scroller_owners).insert(layer_id, scroller_id);
self.mark_layer_canvas_plans_dirty();
}
pub fn get_layer_scroller_owner(&self, layer_id: usize) -> Option<ExpandedNodeIdentifier> {
borrow!(self.layer_scroller_owners).get(&layer_id).copied()
}
fn mark_scroller_content_layers_dirty(&self, scroller_id: u32) {
let layers = borrow!(self.layer_scroller_owners)
.iter()
.filter_map(|(layer, owner)| (owner.to_u32() == scroller_id).then_some(*layer))
.collect::<Vec<_>>();
for layer in layers {
self.set_canvas_dirty(layer);
}
}
pub fn set_root_scroller_id(&self, id: Option<u32>) {
self.root_scroller_id.set(id);
self.invalidate_viewport_presentation();
self.mark_layer_canvas_plans_dirty();
self.mark_occlusion_dirty();
}
pub fn get_root_scroller_id(&self) -> Option<u32> {
self.root_scroller_id.get()
}
pub fn safe_area_insets(&self) -> LocalProperty<SafeAreaInsets> {
self.safe_area_insets.clone()
}
pub fn set_safe_area_insets(&self, insets: SafeAreaInsets) {
let valid = |value: f64| {
if value.is_finite() {
value.max(0.0)
} else {
0.0
}
};
self.safe_area_insets.set_if_neq(SafeAreaInsets {
top: valid(insets.top),
right: valid(insets.right),
bottom: valid(insets.bottom),
left: valid(insets.left),
});
}
pub fn set_visual_viewport_state(&self, state: VisualViewportState) {
self.visual_viewport_state.set(Some(state));
self.invalidate_viewport_presentation();
self.mark_layer_canvas_plans_dirty();
self.mark_occlusion_dirty();
}
pub fn clear_visual_viewport_state(&self) {
self.visual_viewport_state.set(None);
self.invalidate_viewport_presentation();
self.mark_layer_canvas_plans_dirty();
self.mark_occlusion_dirty();
}
pub fn get_visual_viewport_state(&self) -> Option<VisualViewportState> {
self.visual_viewport_state.get()
}
pub fn resize_canvas_layers_to(&self, id: usize) {
let mut dirty_canvases = borrow_mut!(self.dirty_canvases);
let old_len = dirty_canvases.len();
dirty_canvases.resize(id, false);
for dirty in dirty_canvases.iter_mut().skip(old_len) {
*dirty = true;
}
if old_len != id {
self.mark_layer_canvas_plans_dirty();
}
}
pub fn mark_layer_canvas_plans_dirty(&self) {
let next = self.layer_canvas_plan_generation.get().wrapping_add(1);
self.layer_canvas_plan_generation.set(next.max(1));
}
pub fn layer_canvas_plan_generation(&self) -> u32 {
self.layer_canvas_plan_generation.get()
}
pub fn set_canvas_drawable_layers(&self, layers: HashSet<usize>) {
let mut current = borrow_mut!(self.canvas_drawable_layers);
if *current == layers {
return;
}
*current = layers;
self.mark_layer_canvas_plans_dirty();
}
pub fn layer_has_canvas_drawables(&self, layer: usize) -> bool {
borrow!(self.canvas_drawable_layers).contains(&layer)
}
pub fn clear_all_dirty_canvases(&self) {
let mut dirty_canvases = borrow_mut!(self.dirty_canvases);
for v in dirty_canvases.iter_mut() {
*v = false;
}
}
pub fn set_canvas_dirty(&self, id: usize) {
let mut dirty_canvases = borrow_mut!(self.dirty_canvases);
if let Some(v) = dirty_canvases.get_mut(id) {
*v = true;
}
}
pub fn is_canvas_dirty(&self, id: &usize) -> bool {
*borrow!(self.dirty_canvases).get(*id).unwrap_or(&true)
}
pub fn dirty_canvas_layers(&self) -> Vec<usize> {
borrow!(self.dirty_canvases)
.iter()
.enumerate()
.filter_map(|(index, dirty)| dirty.then_some(index))
.collect()
}
pub fn has_dirty_canvas_layers(&self) -> bool {
borrow!(self.dirty_canvases).iter().any(|dirty| *dirty)
}
pub fn has_dirty_canvas_nodes(&self) -> bool {
!borrow!(self.dirty_canvas_nodes).is_empty()
}
pub fn has_canvas_node_removals(&self) -> bool {
!borrow!(self.removed_canvas_nodes).is_empty()
}
pub fn has_canvas_render_work(&self) -> bool {
self.has_dirty_canvas_layers()
|| self.has_canvas_node_removals()
|| self.scene_lighting_dirty.get()
}
pub fn mark_scene_lighting_dirty(&self) {
self.scene_lighting_dirty.set(true);
}
pub(crate) fn take_scene_lighting_dirty(&self) -> bool {
self.scene_lighting_dirty.replace(false)
}
pub fn mark_occlusion_dirty(&self) {
self.occlusion_dirty.set(true);
self.occlusion_state.borrow_mut().structural = true;
borrow_mut!(self.layer_light_scopes).clear();
if !borrow!(self.scene_lighting_nodes).is_empty()
|| !borrow!(self.canvas_node_light_masks).is_empty()
{
self.set_all_canvases_dirty();
}
}
pub fn mark_node_occlusion_dirty(&self, mut id: u32) {
loop {
borrow_mut!(self.scene_geometry).invalidate_existing(id);
let owner = self
.get_expanded_node_by_eid(ExpandedNodeIdentifier(id))
.and_then(|node| node.render_source_owner());
let Some(owner) = owner else {
break;
};
id = owner.to_u32();
}
self.occlusion_dirty.set(true);
self.occlusion_state.borrow_mut().dirty.insert(id);
}
fn mark_occlusion_presentation_dirty(&self, id: u32) {
self.occlusion_dirty.set(true);
self.occlusion_state.borrow_mut().scrolled.insert(id);
}
pub fn occlusion_stats(&self) -> crate::engine::occlusion::OcclusionStats {
self.occlusion_state.borrow().stats
}
pub fn take_occlusion_dirty(&self) -> bool {
let dirty = self.occlusion_dirty.get();
self.occlusion_dirty.set(false);
dirty
}
pub fn set_all_canvases_dirty(&self) {
let mut dirty_canvases = borrow_mut!(self.dirty_canvases);
for v in dirty_canvases.iter_mut() {
*v = true;
}
}
pub fn mark_canvas_node_dirty(&self, id: ExpandedNodeIdentifier) {
borrow_mut!(self.scene_geometry).invalidate(id.to_u32());
let owner = self
.get_expanded_node_by_eid(id)
.and_then(|node| node.render_source_owner())
.and_then(|id| self.get_expanded_node_by_eid(id));
if let Some(owner) = owner {
owner.changed_listener.invalidate();
return;
}
borrow_mut!(self.dirty_canvas_nodes).insert(id);
#[cfg(feature = "shaders")]
{
let mut parent = self
.get_expanded_node_by_eid(id)
.and_then(|n| n.render_parent_node());
while let Some(node) = parent {
if node.shader_effect_bound.get() {
borrow_mut!(self.dirty_canvas_nodes).insert(node.id);
for ids in borrow_mut!(self.targeted_canvas_replay_node_ids).values_mut() {
ids.remove(&node.id.to_u32());
}
}
parent = node.render_parent_node();
}
}
for ids in borrow_mut!(self.targeted_canvas_replay_node_ids).values_mut() {
ids.remove(&id.to_u32());
}
}
pub fn clear_canvas_node_dirty(&self, id: &ExpandedNodeIdentifier) {
borrow_mut!(self.dirty_canvas_nodes).remove(id);
}
pub fn is_canvas_node_dirty(&self, id: &ExpandedNodeIdentifier) -> bool {
borrow!(self.dirty_canvas_nodes).contains(id)
}
pub fn dirty_canvas_node_ids(&self) -> Vec<ExpandedNodeIdentifier> {
borrow!(self.dirty_canvas_nodes).iter().copied().collect()
}
pub fn mark_canvas_nodes_on_layer_dirty(&self, layer: usize) {
if let Some(ids) = borrow_mut!(self.targeted_canvas_replay_node_ids).get_mut(&layer) {
ids.clear();
}
let node_cache = borrow!(self.node_cache);
let dirty_nodes = &mut *borrow_mut!(self.dirty_canvas_nodes);
for node in node_cache.eid_to_node.values() {
if !node.is_render_source()
&& node.occlusion.get().render_layer_id == layer
&& borrow!(node.instance_node).base().flags().layer == crate::api::Layer::Canvas
{
dirty_nodes.insert(node.id);
}
}
}
pub fn canvas_node_light_mask(&self, id: ExpandedNodeIdentifier) -> u32 {
borrow!(self.canvas_node_light_masks)
.get(&id)
.copied()
.unwrap_or(0)
}
pub fn collect_scene_lighting_for_layer(&self, layer: usize) -> SceneLighting {
if borrow!(self.scene_lighting_nodes).is_empty()
&& borrow!(self.canvas_node_light_masks).is_empty()
{
borrow_mut!(self.lighting_overflow_counts).remove(&layer);
return SceneLighting::with_default_ambient(Vec::new());
}
let layer_to_root_transforms = self.layer_to_root_transforms(layer);
let root_to_target = layer_to_root_transforms[&layer].inverse();
let node_cache = borrow!(self.node_cache);
let mut scoped_lights = Vec::new();
let mut topmost_ambient = None;
for id in borrow!(self.scene_lighting_nodes).iter() {
let Some(node) = node_cache.eid_to_node.get(id) else {
continue;
};
if node.is_render_source() {
continue;
}
let occlusion = node.occlusion.get();
let source_layer = occlusion.render_layer_id;
let Some(source_to_root) = layer_to_root_transforms.get(&source_layer) else {
continue;
};
let (light, ambient) = {
let instance_node = borrow!(node.instance_node);
(
instance_node.resolve_scene_light(node, self),
instance_node.resolve_scene_ambient_light(node, self),
)
};
if let Some(light) = light {
if light.enabled {
scoped_lights.push((
occlusion.z_index,
node.id,
Self::nearest_light_frame(node),
transform_scene_light_for_layer(light, *source_to_root, root_to_target),
));
}
}
if let Some(ambient) = ambient {
let should_replace = topmost_ambient
.as_ref()
.map(|(z_index, id, _)| (occlusion.z_index, node.id) >= (*z_index, *id))
.unwrap_or(true);
if should_replace {
topmost_ambient = Some((occlusion.z_index, node.id, ambient));
}
}
}
scoped_lights
.sort_by(|left, right| right.0.cmp(&left.0).then_with(|| left.1.cmp(&right.1)));
let overflow = scoped_lights
.len()
.saturating_sub(SceneLighting::MAX_LIGHTS);
scoped_lights.truncate(SceneLighting::MAX_LIGHTS);
{
let mut overflow_counts = borrow_mut!(self.lighting_overflow_counts);
if overflow == 0 {
overflow_counts.remove(&layer);
} else if overflow_counts.insert(layer, overflow) != Some(overflow) {
log::warn!(
"canvas layer {layer} has more than {} enabled lights; omitting {overflow}",
SceneLighting::MAX_LIGHTS
);
}
}
let scopes = scoped_lights
.iter()
.map(|(_, id, frame, _)| (*id, *frame))
.collect::<Vec<_>>();
let scopes_changed = borrow!(self.layer_light_scopes).get(&layer) != Some(&scopes);
if scopes_changed {
let mut masks = borrow_mut!(self.canvas_node_light_masks);
if !scopes.is_empty() || !masks.is_empty() {
let mut dirty_nodes = borrow_mut!(self.dirty_canvas_nodes);
for node in node_cache.eid_to_node.values() {
#[cfg(test)]
self.lighting_mask_node_visits
.set(self.lighting_mask_node_visits.get() + 1);
if node.occlusion.get().render_layer_id != layer
|| borrow!(node.instance_node).base().flags().layer
!= crate::api::Layer::Canvas
{
continue;
}
let mask = if scopes.is_empty() {
0
} else {
let ancestry = Self::light_frame_ancestry(node);
scopes
.iter()
.enumerate()
.fold(0u32, |mask, (slot, (_, owner))| {
if light_reaches_frame_ancestry(owner.as_ref(), &ancestry) {
mask | (1u32 << slot)
} else {
mask
}
})
};
let previous = if mask == 0 {
masks.remove(&node.id)
} else {
masks.insert(node.id, mask)
}
.unwrap_or(0);
if previous != mask {
self.set_canvas_dirty(layer);
dirty_nodes.insert(node.id);
if let Some(ids) =
borrow_mut!(self.targeted_canvas_replay_node_ids).get_mut(&layer)
{
ids.remove(&node.id.to_u32());
}
}
}
}
borrow_mut!(self.layer_light_scopes).insert(layer, scopes);
}
drop(node_cache);
let lights = scoped_lights
.into_iter()
.map(|(_, _, _, light)| light)
.collect::<Vec<_>>();
if let Some((_, _, ambient)) = topmost_ambient {
SceneLighting {
active: true,
ambient_is_authored: true,
ambient,
lights,
}
} else {
SceneLighting::with_default_ambient(lights)
}
}
fn nearest_light_frame(node: &ExpandedNode) -> Option<ExpandedNodeIdentifier> {
let mut current = node.render_parent_node();
while let Some(parent) = current {
if borrow!(parent.instance_node).establishes_light_frame() {
return Some(parent.id);
}
current = parent.render_parent_node();
}
None
}
fn light_frame_ancestry(node: &ExpandedNode) -> HashSet<ExpandedNodeIdentifier> {
let mut frames = HashSet::new();
let mut current = node.render_parent_node();
while let Some(parent) = current {
if borrow!(parent.instance_node).establishes_light_frame() {
frames.insert(parent.id);
}
current = parent.render_parent_node();
}
frames
}
fn layer_to_root_transforms(&self, layer: usize) -> HashMap<usize, Affine> {
let mut transforms = HashMap::new();
transforms.insert(layer, self.layer_to_root_transform(layer));
let mut current_layer = layer;
let mut visited = HashSet::new();
while let Some(owner_id) = self.get_layer_scroller_owner(current_layer) {
if !visited.insert(current_layer) {
break;
}
let Some(owner_node) = self.get_expanded_node_by_eid(owner_id) else {
break;
};
let owner_layer = owner_node.occlusion.get().render_layer_id;
transforms
.entry(owner_layer)
.or_insert_with(|| self.layer_to_root_transform(owner_layer));
if owner_layer == current_layer {
break;
}
current_layer = owner_layer;
}
transforms
}
fn layer_to_root_transform(&self, layer: usize) -> Affine {
let mut transform = Affine::IDENTITY;
let mut current_layer = layer;
let mut visited = HashSet::new();
while let Some(owner_id) = self.get_layer_scroller_owner(current_layer) {
if !visited.insert(current_layer) {
break;
}
let Some(owner_node) = self.get_expanded_node_by_eid(owner_id) else {
break;
};
let owner_layer = owner_node.occlusion.get().render_layer_id;
let owner_transform = self.canvas_surface_transform_for_node(&owner_node);
let (scroll_x, scroll_y) = self
.get_scroller_surface_scroll(owner_node.id.to_u32())
.or_else(|| borrow!(owner_node.instance_node).resolve_scroll_offset(&owner_node))
.unwrap_or((0.0, 0.0));
transform = owner_transform * Affine::translate((-scroll_x, -scroll_y)) * transform;
if owner_layer == current_layer {
break;
}
current_layer = owner_layer;
}
transform
}
pub fn canvas_surface_transform_for_node(&self, node: &ExpandedNode) -> Affine {
let transform = Affine::from(node.transform_and_bounds.get().transform);
if node.is_render_source() {
return transform;
}
let own_layer = node.occlusion.get().render_layer_id;
self.get_layer_scroller_owner(own_layer)
.and_then(|owner| self.get_expanded_node_by_eid(owner))
.map_or(transform, |owner| {
Affine::from(owner.transform_and_bounds.get().transform.inverse()) * transform
})
}
pub(crate) fn invalidate_scene_geometry(&self, id: u32) {
borrow_mut!(self.scene_geometry).invalidate(id);
}
pub(crate) fn remove_observation_geometry(&self, id: u32) {
if let Some(node) = self.get_expanded_node_by_eid(ExpandedNodeIdentifier(id)) {
let instance = borrow!(node.instance_node);
if instance.base().flags().layer != crate::api::Layer::DontCare
|| instance.materializes_native_surface(&node)
{
return;
}
}
borrow_mut!(self.scene_geometry).remove(id);
}
pub fn prepare_scene_geometry(&self) {
let dirty = borrow_mut!(self.scene_geometry).take_dirty();
for id in dirty {
if borrow!(self.scene_geometry).get(id).is_some() {
continue;
}
if let Some(node) = self.get_expanded_node_by_eid(ExpandedNodeIdentifier(id)) {
if borrow!(node.instance_node).base().flags().layer != crate::api::Layer::DontCare
|| borrow!(node.instance_node).materializes_native_surface(&node)
|| self.viewport_dependency(id)
{
self.prepare_canvas_geometry(&node);
} else {
borrow_mut!(self.scene_geometry).remove(id);
}
}
}
}
fn prepare_canvas_geometry(
&self,
node: &ExpandedNode,
) -> Rc<crate::scene_geometry::PreparedCanvasGeometry> {
let tab = node.transform_and_bounds.get();
let local = borrow!(node.instance_node).prepare_canvas_geometry(node);
let mut prepared = crate::scene_geometry::PreparedCanvasGeometry::new(
node.occlusion.get().render_layer_id,
tab.bounds,
self.canvas_surface_transform_for_node(node),
local,
);
prepared.world_transform = Affine::from(tab.transform);
prepared.is_canvas = !node.is_render_source()
&& borrow!(node.instance_node).base().flags().layer == crate::api::Layer::Canvas;
prepared.is_native = !node.is_render_source()
&& (matches!(
borrow!(node.instance_node).base().flags().layer,
crate::api::Layer::Native | crate::api::Layer::NativeNonOccluding
) || borrow!(node.instance_node).materializes_native_surface(node));
borrow_mut!(self.scene_geometry).insert(node.id.to_u32(), prepared)
}
pub fn canvas_geometry_for_node(
&self,
node: &ExpandedNode,
) -> Rc<crate::scene_geometry::PreparedCanvasGeometry> {
let cached = borrow!(self.scene_geometry).get(node.id.to_u32());
cached.unwrap_or_else(|| self.prepare_canvas_geometry(node))
}
pub fn scene_geometry_stats(&self) -> crate::scene_geometry::SceneGeometryStats {
borrow!(self.scene_geometry).stats
}
pub fn canvas_nodes_intersecting(
&self,
layer: usize,
regions: &[kurbo::Rect],
) -> Option<Vec<u32>> {
self.prepare_scene_geometry();
borrow_mut!(self.scene_geometry).query(layer, regions)
}
pub(crate) fn compositing_nodes_intersecting(
&self,
layer: usize,
regions: &[kurbo::Rect],
native: bool,
) -> Vec<u32> {
borrow_mut!(self.scene_geometry).query_exact(layer, regions, native)
}
pub fn request_canvas_replay(&self, update: pax_runtime_api::ReplayCanvasLayerUpdate) {
if let Some(ids) = update
.regions
.as_deref()
.and_then(|regions| self.canvas_nodes_intersecting(update.layer, regions))
{
self.mark_targeted_canvas_replay_nodes(update.layer, &ids);
self.mark_canvas_nodes_on_layer_dirty_by_id(update.layer, &ids);
} else {
self.mark_canvas_nodes_on_layer_dirty(update.layer);
}
self.set_canvas_dirty(update.layer);
}
pub fn mark_canvas_nodes_on_layer_dirty_by_id(&self, layer: usize, node_ids: &[u32]) {
let node_cache = borrow!(self.node_cache);
let dirty_nodes = &mut *borrow_mut!(self.dirty_canvas_nodes);
for node_id in node_ids {
let id = ExpandedNodeIdentifier(*node_id);
let Some(node) = node_cache.eid_to_node.get(&id) else {
continue;
};
if node.occlusion.get().render_layer_id == layer
&& borrow!(node.instance_node).base().flags().layer == crate::api::Layer::Canvas
{
dirty_nodes.insert(id);
}
}
}
pub fn mark_targeted_canvas_replay_nodes(&self, layer: usize, node_ids: &[u32]) {
let mut targeted = borrow_mut!(self.targeted_canvas_replay_node_ids);
let layer_nodes = targeted.entry(layer).or_default();
let dirty = borrow!(self.dirty_canvas_nodes);
for id in node_ids {
if !dirty.contains(&ExpandedNodeIdentifier(*id)) {
layer_nodes.insert(*id);
}
}
}
pub fn take_targeted_canvas_replay_node_ids(&self) -> HashMap<usize, HashSet<u32>> {
let targeted = &mut *borrow_mut!(self.targeted_canvas_replay_node_ids);
std::mem::take(targeted)
}
pub fn mark_all_canvas_nodes_dirty(&self) {
for ids in borrow_mut!(self.targeted_canvas_replay_node_ids).values_mut() {
ids.clear();
}
let node_cache = borrow!(self.node_cache);
let dirty_nodes = &mut *borrow_mut!(self.dirty_canvas_nodes);
for node in node_cache.eid_to_node.values() {
if !node.is_render_source()
&& borrow!(node.instance_node).base().flags().layer == crate::api::Layer::Canvas
{
dirty_nodes.insert(node.id);
}
}
}
pub fn enqueue_canvas_node_removal(&self, layer: usize, node_id: u32) {
borrow_mut!(self.removed_canvas_nodes).push((layer, node_id));
}
pub fn take_canvas_node_removals(&self) -> Vec<(usize, u32)> {
let mut removals = borrow_mut!(self.removed_canvas_nodes);
std::mem::take(&mut *removals)
}
pub fn get_expanded_nodes_by_id(&self, id: &str) -> Vec<Rc<ExpandedNode>> {
borrow!(self.node_cache)
.eid_to_node
.values()
.filter(|val| {
let common_props = val.get_common_properties();
let common_props = borrow!(common_props);
common_props.id.get().is_some_and(|i| i == id)
})
.cloned()
.collect()
}
pub fn get_expanded_nodes_by_global_ids(
&self,
uni: &UniqueTemplateNodeIdentifier,
) -> Vec<Rc<ExpandedNode>> {
let node_cache = borrow!(self.node_cache);
node_cache
.uni_to_eid
.get(uni)
.map(|eids| {
let mut nodes = vec![];
for e in eids {
if let Some(node) = node_cache.eid_to_node.get(e) {
nodes.push(Rc::clone(node));
} else {
log::warn!(
"failed to find node in engine for expanded node identifier: {:?}",
e
);
}
}
nodes
})
.unwrap_or_default()
}
pub fn get_elements_beneath_ray(
&self,
root: Option<Rc<ExpandedNode>>,
ray: Point2<Window>,
limit_one: bool,
mut accum: Vec<Rc<ExpandedNode>>,
hit_invisible: bool,
) -> Vec<Rc<ExpandedNode>> {
let Some(root_node) = root.or_else(|| borrow!(self.root_expanded_node).upgrade()) else {
return accum;
};
let mut to_process = vec![(root_node, false, Affine::IDENTITY, false)];
while let Some((node, clipped, active_scroll_transform, retained_for_exit)) =
to_process.pop()
{
if node.is_render_source() {
continue;
}
node.compute_flattened_projected_children();
if !hit_invisible
&& (retained_for_exit || node.transition_phase.get() == TRANSITION_PHASE_EXIT)
{
continue;
}
let (scroll_transform, clips_content) = {
let instance_node = borrow!(node.instance_node);
let scrolls_content = instance_node.scrolls_content(&node);
let clips_content = instance_node.clips_content(&node);
drop(instance_node);
(
if scrolls_content {
self.scroller_content_presentation_transform(&node)
} else {
Affine::IDENTITY
},
clips_content,
)
};
let descendant_scroll_transform = active_scroll_transform * scroll_transform;
let hit = node.ray_cast_test(active_scroll_transform.inverse() * ray);
if hit && !clipped {
if hit_invisible
|| !borrow!(node.instance_node)
.base()
.flags()
.invisible_to_raycasting
{
if limit_one {
return vec![node];
}
accum.push(Rc::clone(&node));
}
}
let clipped = clipped || (!hit && clips_content);
let retained_children = borrow!(node.exiting_children).clone();
to_process.extend(
node.children
.get()
.iter()
.cloned()
.map(|v| {
let retained_for_exit = retained_children
.iter()
.any(|retained| Rc::ptr_eq(retained, &v));
let cp = v.get_common_properties();
let unclippable = borrow!(cp).unclippable.get().unwrap_or(false);
(
v,
clipped && !unclippable,
descendant_scroll_transform,
retained_for_exit,
)
})
.rev(),
)
}
accum
}
pub fn get_topmost_element_beneath_ray(
self: &Rc<Self>,
ray: Point2<Window>,
) -> Option<Rc<ExpandedNode>> {
let res = self.get_elements_beneath_ray(None, ray, true, vec![], false);
let Some(new_topmost) = res
.into_iter()
.next()
.or_else(|| borrow!(self.root_expanded_node).upgrade())
else {
*borrow_mut!(self.last_topmost_element) = Weak::new();
return None;
};
let last_topmost = borrow!(self.last_topmost_element).upgrade();
if Some(new_topmost.id) != last_topmost.as_ref().map(|n| n.id) {
let (leaving, entering) = find_template_paths_to_common_ancestor(
&last_topmost,
&Some(Rc::clone(&new_topmost)),
);
for leave in leaving {
leave.dispatch_mouse_out(Event::new(MouseOut {}), &self.globals(), self);
}
for enter in entering {
enter.dispatch_mouse_over(Event::new(MouseOver {}), &self.globals(), self);
}
*borrow_mut!(self.last_topmost_element) = Rc::downgrade(&new_topmost);
}
Some(new_topmost)
}
pub fn gen_uid(&self) -> ExpandedNodeIdentifier {
let val = self.next_uid.get();
let next_val = ExpandedNodeIdentifier(val.0 + 1);
self.next_uid.set(next_val);
val
}
pub fn enqueue_native_message(&self, message: NativeMessage) {
borrow_mut!(self.messages).push(message)
}
pub fn take_native_messages(&self) -> Vec<NativeMessage> {
let mut messages = borrow_mut!(self.messages);
std::mem::take(&mut *messages)
}
pub fn globals(&self) -> Globals {
borrow!(self.globals).clone()
}
pub fn edit_globals(&self, f: impl Fn(&mut Globals)) {
let mut globals = borrow_mut!(self.globals);
f(&mut globals);
}
pub fn queue_custom_event(&self, source_expanded_node: Rc<ExpandedNode>, name: &'static str) {
let mut queued_custom_events = borrow_mut!(self.queued_custom_events);
queued_custom_events.push((source_expanded_node, name));
}
pub fn flush_custom_events(self: &Rc<Self>) -> Result<(), String> {
let mut queued_custom_event = borrow_mut!(self.queued_custom_events);
let to_flush: Vec<_> = std::mem::take(queued_custom_event.as_mut());
for (target, ident) in to_flush {
target.dispatch_custom_event(ident, self)?;
}
Ok(())
}
#[cfg(feature = "designtime")]
pub fn get_userland_root_expanded_node(&self) -> Option<Rc<ExpandedNode>> {
borrow!(self.userland_root_expanded_node).clone()
}
#[cfg(feature = "designtime")]
pub fn get_userland_root_instance_node(&self) -> Option<Rc<dyn InstanceNode>> {
borrow!(self.userland_frame_instance_node).clone()
}
#[cfg(feature = "designtime")]
pub fn set_userland_root_expanded_node(&self, root: Option<Rc<ExpandedNode>>) {
*borrow_mut!(self.userland_root_expanded_node) = root;
}
#[cfg(feature = "designtime")]
pub fn set_userland_root_instance_node(&self, node: Option<Rc<dyn InstanceNode>>) {
*borrow_mut!(self.userland_frame_instance_node) = node;
}
pub fn get_root_expanded_node(&self) -> Option<Rc<ExpandedNode>> {
borrow!(self.root_expanded_node).upgrade()
}
pub fn queue_render(&self, expanded_node: Rc<ExpandedNode>) {
borrow_mut!(self.queued_renders).push(expanded_node);
}
pub fn recurse_flush_queued_renders(self: &Rc<RuntimeContext>, rcs: &mut dyn RenderContext) {
while !borrow!(self.queued_renders).is_empty() {
for n in std::mem::take(&mut *borrow_mut!(self.queued_renders)) {
n.recurse_render(self, rcs);
}
}
}
}
fn find_template_paths_to_common_ancestor(
last_topmost: &Option<Rc<ExpandedNode>>,
new_topmost: &Option<Rc<ExpandedNode>>,
) -> (Vec<Rc<ExpandedNode>>, Vec<Rc<ExpandedNode>>) {
let mut last_path = template_path_from_root(last_topmost);
let mut new_path = template_path_from_root(new_topmost);
let common_prefix_len = last_path
.iter()
.zip(new_path.iter())
.take_while(|(last, new)| last.id == new.id)
.count();
last_path.drain(0..common_prefix_len);
new_path.drain(0..common_prefix_len);
(last_path, new_path)
}
fn template_path_from_root(node: &Option<Rc<ExpandedNode>>) -> Vec<Rc<ExpandedNode>> {
let mut path = Vec::new();
let mut current = node.clone();
while let Some(node) = current {
path.push(Rc::clone(&node));
current = node.template_parent.upgrade();
}
path.reverse();
path
}
pub struct RuntimePropertiesStackFrame {
symbols_within_frame: HashMap<String, Variable>,
parent: Option<Rc<RuntimePropertiesStackFrame>>,
}
impl RuntimePropertiesStackFrame {
pub fn new(symbols_within_frame: HashMap<String, Variable>) -> Rc<Self> {
Rc::new(Self {
symbols_within_frame,
parent: None,
})
}
pub fn push(self: &Rc<Self>, symbols_within_frame: HashMap<String, Variable>) -> Rc<Self> {
Rc::new(RuntimePropertiesStackFrame {
symbols_within_frame,
parent: Some(Rc::clone(self)),
})
}
pub fn pop(self: &Rc<Self>) -> Option<Rc<Self>> {
self.parent.clone()
}
pub fn resolve_symbol_as_variable(&self, symbol: &str) -> Option<Variable> {
if let Some(e) = self.symbols_within_frame.get(&clean_symbol(symbol)) {
Some(e.clone())
} else {
self.parent.as_ref()?.resolve_symbol_as_variable(symbol)
}
}
pub fn resolve_symbol_as_erased_property(&self, symbol: &str) -> Option<UntypedProperty> {
if let Some(e) = self.symbols_within_frame.get(&clean_symbol(symbol)) {
Some(e.clone().get_untyped_property().clone())
} else {
self.parent
.as_ref()?
.resolve_symbol_as_erased_property(symbol)
}
}
pub fn resolve_symbol(&self, symbol: &str) -> Option<Variable> {
self.symbols_within_frame
.get(&clean_symbol(symbol))
.cloned()
.or_else(|| self.parent.as_ref()?.resolve_symbol(symbol))
}
}
fn clean_symbol(symbol: &str) -> String {
symbol.replace("self.", "").replace("this.", "")
}
impl IdentifierResolver for RuntimePropertiesStackFrame {
fn resolve(&self, name: String) -> Result<Variable, String> {
self.resolve_symbol(&name)
.ok_or_else(|| format!("Could not resolve symbol {}", name))
}
}
fn light_reaches_frame_ancestry(
owner_frame: Option<&ExpandedNodeIdentifier>,
frame_ancestry: &HashSet<ExpandedNodeIdentifier>,
) -> bool {
owner_frame.is_none_or(|frame| frame_ancestry.contains(frame))
}
pub struct ExpressionContext {
pub stack_frame: Rc<RuntimePropertiesStackFrame>,
}
#[cfg(test)]
mod light_scope_tests {
use super::*;
use crate::api::math::Transform2;
use crate::{
BaseInstance, CommonPropertiesInit, ComponentInstance, InstanceFlags, InstanceNode,
InstantiationArgs, PropertiesInit, PropertiesScopeInit, RouteLocation, TransformAndBounds,
};
use pax_runtime_api::pax_value::{PaxAny, PaxValue};
use pax_runtime_api::{Layer, Platform, SceneAmbientLight, TargetInfo, OS};
use std::fmt;
#[derive(Clone, Copy)]
enum TestLightingRole {
Frame,
Light(f64),
Ambient(f64),
Surface,
}
struct TestLightingNode {
base: BaseInstance,
role: TestLightingRole,
enabled: Cell<bool>,
intensity_multiplier: Cell<f64>,
resolutions: Cell<usize>,
}
impl TestLightingNode {
fn new(
role: TestLightingRole,
children: Vec<Rc<dyn InstanceNode>>,
) -> Rc<TestLightingNode> {
Rc::new(Self {
base: BaseInstance::new(
node_args(children),
InstanceFlags {
invisible_to_slot: false,
invisible_to_raycasting: true,
layer: match role {
TestLightingRole::Frame => Layer::DontCare,
TestLightingRole::Light(_)
| TestLightingRole::Ambient(_)
| TestLightingRole::Surface => Layer::Canvas,
},
is_component: false,
is_slot: false,
},
),
role,
enabled: Cell::new(true),
intensity_multiplier: Cell::new(1.0),
resolutions: Cell::new(0),
})
}
fn as_instance(self: &Rc<Self>) -> Rc<dyn InstanceNode> {
self.clone()
}
fn set_enabled(&self, enabled: bool) {
self.enabled.set(enabled);
}
}
impl InstanceNode for TestLightingNode {
fn has_scene_lighting(&self) -> bool {
matches!(
self.role,
TestLightingRole::Light(_) | TestLightingRole::Ambient(_)
)
}
fn instantiate(args: InstantiationArgs) -> Rc<Self>
where
Self: Sized,
{
Rc::new(Self {
base: BaseInstance::new(
args,
InstanceFlags {
invisible_to_slot: false,
invisible_to_raycasting: true,
layer: Layer::Canvas,
is_component: false,
is_slot: false,
},
),
role: TestLightingRole::Surface,
enabled: Cell::new(true),
intensity_multiplier: Cell::new(1.0),
resolutions: Cell::new(0),
})
}
fn resolve_debug(
&self,
f: &mut fmt::Formatter,
_expanded_node: Option<&ExpandedNode>,
) -> fmt::Result {
f.debug_struct("TestLightingNode").finish()
}
fn resolve_scene_light(
&self,
_expanded_node: &ExpandedNode,
_context: &RuntimeContext,
) -> Option<SceneLight> {
assert!(self.has_scene_lighting());
self.resolutions.set(self.resolutions.get() + 1);
match self.role {
TestLightingRole::Light(intensity) if self.enabled.get() => Some(SceneLight {
intensity: intensity * self.intensity_multiplier.get(),
..Default::default()
}),
_ => None,
}
}
fn resolve_scene_ambient_light(
&self,
_expanded_node: &ExpandedNode,
_context: &RuntimeContext,
) -> Option<SceneAmbientLight> {
assert!(self.has_scene_lighting());
self.resolutions.set(self.resolutions.get() + 1);
match self.role {
TestLightingRole::Ambient(intensity) if self.enabled.get() => {
Some(SceneAmbientLight {
intensity,
..Default::default()
})
}
_ => None,
}
}
fn establishes_light_frame(&self) -> bool {
matches!(self.role, TestLightingRole::Frame)
}
fn base(&self) -> &BaseInstance {
&self.base
}
}
struct ScopedLightingFixture {
context: Rc<RuntimeContext>,
_root: Rc<ExpandedNode>,
root_light: Rc<TestLightingNode>,
outer_light: Rc<TestLightingNode>,
ambient: Rc<TestLightingNode>,
root_surface: Rc<ExpandedNode>,
outer_surface: Rc<ExpandedNode>,
nested_surface: Rc<ExpandedNode>,
sibling_surface: Rc<ExpandedNode>,
sibling_frame: Rc<ExpandedNode>,
}
fn test_globals() -> Globals {
Globals {
elapsed_frames: LocalProperty::new(0),
elapsed_millis: LocalProperty::new(0),
viewport: LocalProperty::new(TransformAndBounds {
transform: Transform2::identity(),
bounds: (100.0, 100.0),
}),
gyro: LocalProperty::new(Default::default()),
accel: LocalProperty::new(Default::default()),
route_location: LocalProperty::new(RouteLocation::root()),
browser_allows_scroller_vector_layers: LocalProperty::new(true),
browser_allows_nested_scroller_vector_layers: LocalProperty::new(true),
platform: Platform::Unknown,
os: OS::Unknown,
target: TargetInfo::new(Platform::Unknown, OS::Unknown),
#[cfg(feature = "designtime")]
designtime: crate::test_support::designtime(),
get_elapsed_millis: Rc::new(|| 0),
}
}
fn properties_factory() -> crate::PropertiesFactory {
Box::new(|_, expanded_node| {
expanded_node
.is_none()
.then(|| Rc::new(RefCell::new(PaxAny::Builtin(PaxValue::default()))))
})
}
fn node_args(children: Vec<Rc<dyn InstanceNode>>) -> InstantiationArgs {
InstantiationArgs {
prototypical_common_properties: CommonPropertiesInit::Default,
prototypical_properties: PropertiesInit::Factory(properties_factory()),
handler_registry: None,
children: Some(RefCell::new(children)),
component_template: None,
component_settings: None,
template_node_identifier: None,
template_node_type_id: None,
template_node_selector_info: None,
transition_config: Default::default(),
properties_scope: PropertiesScopeInit::None,
}
}
fn component_args(template: Vec<Rc<dyn InstanceNode>>) -> InstantiationArgs {
InstantiationArgs {
prototypical_common_properties: CommonPropertiesInit::Default,
prototypical_properties: PropertiesInit::Factory(properties_factory()),
handler_registry: None,
children: None,
component_template: Some(RefCell::new(template)),
component_settings: None,
template_node_identifier: None,
template_node_type_id: None,
template_node_selector_info: None,
transition_config: Default::default(),
properties_scope: PropertiesScopeInit::None,
}
}
fn mount_test_tree(
children: Vec<Rc<dyn InstanceNode>>,
) -> (Rc<RuntimeContext>, Rc<ExpandedNode>) {
let root_component = ComponentInstance::instantiate(component_args(children));
let context = Rc::new(crate::test_support::runtime_context(test_globals()));
let root = ExpandedNode::initialize_root(root_component, &context);
root.recurse_update(&context);
crate::engine::occlusion::update_node_occlusion(&root, &context);
(context, root)
}
fn clear_dirty_canvas_nodes(context: &RuntimeContext) {
for id in context.dirty_canvas_node_ids() {
context.clear_canvas_node_dirty(&id);
}
}
fn scoped_lighting_fixture() -> ScopedLightingFixture {
let nested_light = TestLightingNode::new(TestLightingRole::Light(3.0), Vec::new());
let nested_surface = TestLightingNode::new(TestLightingRole::Surface, Vec::new());
let nested_frame = TestLightingNode::new(
TestLightingRole::Frame,
vec![nested_light.as_instance(), nested_surface.as_instance()],
);
let sibling_surface = TestLightingNode::new(TestLightingRole::Surface, Vec::new());
let sibling_frame =
TestLightingNode::new(TestLightingRole::Frame, vec![sibling_surface.as_instance()]);
let outer_light = TestLightingNode::new(TestLightingRole::Light(2.0), Vec::new());
let outer_surface = TestLightingNode::new(TestLightingRole::Surface, Vec::new());
let outer_frame = TestLightingNode::new(
TestLightingRole::Frame,
vec![
outer_light.as_instance(),
outer_surface.as_instance(),
nested_frame.as_instance(),
sibling_frame.as_instance(),
],
);
let root_light = TestLightingNode::new(TestLightingRole::Light(1.0), Vec::new());
let root_surface = TestLightingNode::new(TestLightingRole::Surface, Vec::new());
let ambient = TestLightingNode::new(TestLightingRole::Ambient(0.72), Vec::new());
ambient.set_enabled(false);
let (context, root) = mount_test_tree(vec![
root_light.as_instance(),
root_surface.as_instance(),
outer_frame.as_instance(),
ambient.as_instance(),
]);
let root_children = root.children.get();
let root_surface_node = root_children[1].clone();
let outer_frame_node = root_children[2].clone();
let outer_children = outer_frame_node.children.get();
let outer_surface_node = outer_children[1].clone();
let nested_frame_node = outer_children[2].clone();
let sibling_frame_node = outer_children[3].clone();
let nested_children = nested_frame_node.children.get();
let nested_surface_node = nested_children[1].clone();
let sibling_surface_node = sibling_frame_node.children.get()[0].clone();
ScopedLightingFixture {
context,
_root: root,
root_light,
outer_light,
ambient,
root_surface: root_surface_node,
outer_surface: outer_surface_node,
nested_surface: nested_surface_node,
sibling_surface: sibling_surface_node,
sibling_frame: sibling_frame_node,
}
}
#[test]
fn root_lights_reach_every_frame_ancestry() {
let ancestry = HashSet::from([ExpandedNodeIdentifier(10), ExpandedNodeIdentifier(20)]);
assert!(light_reaches_frame_ancestry(None, &ancestry));
}
#[test]
fn framed_lights_reach_only_descendants_of_their_owner_frame() {
let outer_frame = ExpandedNodeIdentifier(10);
let nested_frame = ExpandedNodeIdentifier(20);
let sibling_frame = ExpandedNodeIdentifier(30);
let nested_ancestry = HashSet::from([outer_frame, nested_frame]);
assert!(light_reaches_frame_ancestry(
Some(&outer_frame),
&nested_ancestry
));
assert!(light_reaches_frame_ancestry(
Some(&nested_frame),
&nested_ancestry
));
assert!(!light_reaches_frame_ancestry(
Some(&sibling_frame),
&nested_ancestry
));
}
#[test]
fn mounted_tree_resolves_root_nested_and_sibling_light_masks() {
let fixture = scoped_lighting_fixture();
let lighting = fixture.context.collect_scene_lighting_for_layer(0);
assert_eq!(
lighting
.lights
.iter()
.map(|light| light.intensity)
.collect::<Vec<_>>(),
vec![1.0, 2.0, 3.0]
);
assert!(!lighting.ambient_is_authored);
assert_eq!(
fixture
.context
.canvas_node_light_mask(fixture.root_surface.id),
0b001
);
assert_eq!(
fixture
.context
.canvas_node_light_mask(fixture.outer_surface.id),
0b011
);
assert_eq!(
fixture
.context
.canvas_node_light_mask(fixture.nested_surface.id),
0b111
);
assert_eq!(
fixture
.context
.canvas_node_light_mask(fixture.sibling_surface.id),
0b011
);
}
#[test]
fn unlit_scene_does_not_resolve_nonproviders_or_store_zero_masks() {
let surfaces = (0..1000)
.map(|_| TestLightingNode::new(TestLightingRole::Surface, Vec::new()))
.collect::<Vec<_>>();
let (context, _root) =
mount_test_tree(surfaces.iter().map(TestLightingNode::as_instance).collect());
clear_dirty_canvas_nodes(&context);
for _ in 0..100 {
let lighting = context.collect_scene_lighting_for_layer(0);
assert!(lighting.lights.is_empty());
assert!(!lighting.ambient_is_authored);
assert_eq!(
lighting.ambient.intensity,
SceneLighting::DEFAULT_AMBIENT_INTENSITY
);
}
assert!(surfaces
.iter()
.all(|surface| surface.resolutions.get() == 0));
assert!(borrow!(context.canvas_node_light_masks).is_empty());
assert!(context.dirty_canvas_node_ids().is_empty());
}
#[test]
fn disabled_provider_enable_remove_and_remount_refresh_memberships() {
let light = TestLightingNode::new(TestLightingRole::Light(2.0), Vec::new());
light.set_enabled(false);
let surface = TestLightingNode::new(TestLightingRole::Surface, Vec::new());
let (context, root) = mount_test_tree(vec![light.as_instance(), surface.as_instance()]);
let children = root.children.get();
let light_node = children[0].clone();
let surface_node = &children[1];
assert!(context
.collect_scene_lighting_for_layer(0)
.lights
.is_empty());
light.set_enabled(true);
assert_eq!(context.collect_scene_lighting_for_layer(0).lights.len(), 1);
assert_eq!(context.canvas_node_light_mask(surface_node.id), 1);
clear_dirty_canvas_nodes(&context);
light_node.clone().recurse_unmount(&context);
assert!(context
.collect_scene_lighting_for_layer(0)
.lights
.is_empty());
assert_eq!(context.canvas_node_light_mask(surface_node.id), 0);
assert!(context.is_canvas_node_dirty(&surface_node.id));
assert!(borrow!(context.scene_lighting_nodes).is_empty());
assert!(borrow!(context.canvas_node_light_masks).is_empty());
light_node.recurse_mount(&context);
assert_eq!(context.collect_scene_lighting_for_layer(0).lights.len(), 1);
assert_eq!(context.canvas_node_light_mask(surface_node.id), 1);
}
#[test]
fn template_replacement_refreshes_provider_capability_without_remount() {
let surface = TestLightingNode::new(TestLightingRole::Surface, Vec::new());
let (context, root) = mount_test_tree(vec![surface.as_instance()]);
let node = root.children.get()[0].clone();
assert!(context
.collect_scene_lighting_for_layer(0)
.lights
.is_empty());
let light = TestLightingNode::new(TestLightingRole::Light(3.0), Vec::new());
node.recreate_with_new_data(light.as_instance(), &context);
assert_eq!(
context.collect_scene_lighting_for_layer(0).lights[0].intensity,
3.0
);
node.recreate_with_new_data(surface.as_instance(), &context);
assert!(context
.collect_scene_lighting_for_layer(0)
.lights
.is_empty());
assert_eq!(context.canvas_node_light_mask(node.id), 0);
assert!(borrow!(context.scene_lighting_nodes).is_empty());
}
#[test]
fn slot_reallocation_and_render_reparenting_refresh_retained_masks() {
let fixture = scoped_lighting_fixture();
fixture.context.collect_scene_lighting_for_layer(0);
clear_dirty_canvas_nodes(&fixture.context);
fixture.outer_light.set_enabled(false);
let lighting = fixture.context.collect_scene_lighting_for_layer(0);
assert_eq!(
lighting
.lights
.iter()
.map(|light| light.intensity)
.collect::<Vec<_>>(),
vec![1.0, 3.0]
);
assert_eq!(
fixture
.context
.canvas_node_light_mask(fixture.root_surface.id),
0b01
);
assert_eq!(
fixture
.context
.canvas_node_light_mask(fixture.outer_surface.id),
0b01
);
assert_eq!(
fixture
.context
.canvas_node_light_mask(fixture.nested_surface.id),
0b11
);
assert!(fixture
.context
.is_canvas_node_dirty(&fixture.outer_surface.id));
assert!(fixture
.context
.is_canvas_node_dirty(&fixture.nested_surface.id));
assert!(!fixture
.context
.is_canvas_node_dirty(&fixture.root_surface.id));
clear_dirty_canvas_nodes(&fixture.context);
*borrow_mut!(fixture.nested_surface.render_parent) = Rc::downgrade(&fixture.sibling_frame);
fixture.context.mark_occlusion_dirty();
fixture.context.collect_scene_lighting_for_layer(0);
assert_eq!(
fixture
.context
.canvas_node_light_mask(fixture.nested_surface.id),
0b01
);
assert!(fixture
.context
.is_canvas_node_dirty(&fixture.nested_surface.id));
}
#[test]
fn default_and_authored_ambient_keep_empty_direct_masks_distinct() {
let fixture = scoped_lighting_fixture();
fixture.root_light.set_enabled(false);
let default_ambient = fixture.context.collect_scene_lighting_for_layer(0);
assert!(default_ambient.active);
assert!(!default_ambient.ambient_is_authored);
assert_eq!(
fixture
.context
.canvas_node_light_mask(fixture.root_surface.id),
0
);
assert_eq!(
default_ambient.ambient.intensity,
SceneLighting::DEFAULT_AMBIENT_INTENSITY
);
fixture.ambient.set_enabled(true);
let authored_ambient = fixture.context.collect_scene_lighting_for_layer(0);
assert!(authored_ambient.active);
assert!(authored_ambient.ambient_is_authored);
assert_eq!(authored_ambient.ambient.intensity, 0.72);
assert_eq!(
fixture
.context
.canvas_node_light_mask(fixture.root_surface.id),
0
);
}
#[test]
fn light_overflow_selection_is_deterministic_and_recovers() {
let lights = (0..10)
.map(|index| {
TestLightingNode::new(TestLightingRole::Light((index + 1) as f64), Vec::new())
})
.collect::<Vec<_>>();
let surface = TestLightingNode::new(TestLightingRole::Surface, Vec::new());
let mut templates = lights
.iter()
.rev()
.map(TestLightingNode::as_instance)
.collect::<Vec<_>>();
templates.push(surface.as_instance());
let (context, root) = mount_test_tree(templates);
let expanded = root.children.get();
let surface_node = expanded[10].clone();
let first = context.collect_scene_lighting_for_layer(0);
let second = context.collect_scene_lighting_for_layer(0);
let expected = vec![10.0, 9.0, 8.0, 7.0, 6.0, 5.0, 4.0, 3.0];
assert_eq!(
first
.lights
.iter()
.map(|light| light.intensity)
.collect::<Vec<_>>(),
expected
);
assert_eq!(first.lights, second.lights);
assert_eq!(context.canvas_node_light_mask(surface_node.id), 0xff);
assert_eq!(borrow!(context.lighting_overflow_counts).get(&0), Some(&2));
lights[0].set_enabled(false);
lights[1].set_enabled(false);
let recovered = context.collect_scene_lighting_for_layer(0);
assert_eq!(
recovered
.lights
.iter()
.map(|light| light.intensity)
.collect::<Vec<_>>(),
expected
);
assert!(!borrow!(context.lighting_overflow_counts).contains_key(&0));
}
#[test]
fn unlit_scene_does_no_lighting_scan_regardless_of_scene_size() {
let surfaces = (0..1024)
.map(|_| TestLightingNode::new(TestLightingRole::Surface, Vec::new()).as_instance())
.collect();
let (context, _root) = mount_test_tree(surfaces);
clear_dirty_canvas_nodes(&context);
for _ in 0..20 {
context.set_canvas_dirty(0);
let lighting = context.collect_scene_lighting_for_layer(0);
assert!(lighting.lights.is_empty());
assert!(!lighting.ambient_is_authored);
}
assert_eq!(context.lighting_mask_node_visits.get(), 0);
assert!(borrow!(context.canvas_node_light_masks).is_empty());
assert!(context.dirty_canvas_node_ids().is_empty());
}
#[test]
fn lighting_masks_survive_geometry_and_value_changes_but_not_scope_changes() {
let fixture = scoped_lighting_fixture();
fixture.context.collect_scene_lighting_for_layer(0);
clear_dirty_canvas_nodes(&fixture.context);
let visits = fixture.context.lighting_mask_node_visits.get();
fixture.root_light.intensity_multiplier.set(4.0);
fixture
.context
.mark_node_occlusion_dirty(fixture.outer_surface.id.to_u32());
let lighting = fixture.context.collect_scene_lighting_for_layer(0);
assert_eq!(lighting.lights[0].intensity, 4.0);
assert_eq!(fixture.context.lighting_mask_node_visits.get(), visits);
assert!(fixture.context.dirty_canvas_node_ids().is_empty());
let root_light = fixture._root.children.get()[0].clone();
*borrow_mut!(root_light.render_parent) = Rc::downgrade(&fixture.sibling_frame);
fixture.context.mark_occlusion_dirty();
fixture.context.collect_scene_lighting_for_layer(0);
assert_eq!(
fixture
.context
.canvas_node_light_mask(fixture.root_surface.id),
0
);
assert_eq!(
fixture
.context
.canvas_node_light_mask(fixture.sibling_surface.id),
0b011
);
assert!(fixture
.context
.is_canvas_node_dirty(&fixture.root_surface.id));
assert!(fixture.context.lighting_mask_node_visits.get() > visits);
}
#[test]
fn disabled_light_stays_registered_and_last_light_removal_clears_masks() {
let light = TestLightingNode::new(TestLightingRole::Light(2.0), Vec::new());
light.set_enabled(false);
let surface = TestLightingNode::new(TestLightingRole::Surface, Vec::new());
let (context, root) = mount_test_tree(vec![light.as_instance(), surface.as_instance()]);
let nodes = root.children.get();
assert!(context
.collect_scene_lighting_for_layer(0)
.lights
.is_empty());
assert_eq!(context.lighting_mask_node_visits.get(), 0);
light.set_enabled(true);
assert_eq!(context.collect_scene_lighting_for_layer(0).lights.len(), 1);
assert_eq!(context.canvas_node_light_mask(nodes[1].id), 1);
clear_dirty_canvas_nodes(&context);
context.clear_all_dirty_canvases();
nodes[0].clone().recurse_unmount(&context);
assert!(context.is_canvas_dirty(&0));
assert!(context
.collect_scene_lighting_for_layer(0)
.lights
.is_empty());
assert_eq!(context.canvas_node_light_mask(nodes[1].id), 0);
assert!(context.is_canvas_node_dirty(&nodes[1].id));
assert!(borrow!(context.scene_lighting_nodes).is_empty());
let visits = context.lighting_mask_node_visits.get();
context.collect_scene_lighting_for_layer(0);
assert_eq!(context.lighting_mask_node_visits.get(), visits);
}
#[test]
fn ambient_only_scene_never_builds_direct_masks() {
let ambient = TestLightingNode::new(TestLightingRole::Ambient(0.72), Vec::new());
let (context, _root) = mount_test_tree(vec![
ambient.as_instance(),
TestLightingNode::new(TestLightingRole::Surface, Vec::new()).as_instance(),
]);
assert!(
context
.collect_scene_lighting_for_layer(0)
.ambient_is_authored
);
ambient.set_enabled(false);
assert!(
!context
.collect_scene_lighting_for_layer(0)
.ambient_is_authored
);
ambient.set_enabled(true);
assert_eq!(
context
.collect_scene_lighting_for_layer(0)
.ambient
.intensity,
0.72
);
assert_eq!(context.lighting_mask_node_visits.get(), 0);
}
#[test]
fn replacement_refreshes_lighting_capability_in_both_directions() {
let (context, root) = mount_test_tree(vec![TestLightingNode::new(
TestLightingRole::Surface,
Vec::new(),
)
.as_instance()]);
let node = root.children.get()[0].clone();
context.collect_scene_lighting_for_layer(0);
node.recreate_with_new_data(
TestLightingNode::new(TestLightingRole::Light(5.0), Vec::new()).as_instance(),
&context,
);
assert_eq!(
context.collect_scene_lighting_for_layer(0).lights[0].intensity,
5.0
);
node.recreate_with_new_data(
TestLightingNode::new(TestLightingRole::Surface, Vec::new()).as_instance(),
&context,
);
assert!(context
.collect_scene_lighting_for_layer(0)
.lights
.is_empty());
assert_eq!(context.canvas_node_light_mask(node.id), 0);
}
#[test]
fn ancestor_lights_follow_scroll_without_rescanning_and_clear_after_layer_move() {
let light = TestLightingNode::new(TestLightingRole::Light(1.0), Vec::new());
let (context, root) = mount_test_tree(vec![
light.as_instance(),
TestLightingNode::new(TestLightingRole::Surface, Vec::new()).as_instance(),
TestLightingNode::new(TestLightingRole::Surface, Vec::new()).as_instance(),
]);
let nodes = root.children.get();
let surface = &nodes[2];
let mut occlusion = surface.occlusion.get();
occlusion.render_layer_id = 1;
surface.occlusion.set(occlusion);
context.register_layer_scroller_owner(1, nodes[1].id);
context.set_scroller_surface_state(
nodes[1].id.to_u32(),
ScrollerSurfaceState {
viewport_width: 100.0,
viewport_height: 100.0,
content_width: 100.0,
content_height: 1000.0,
clip_content: true,
..Default::default()
},
);
let before = context.collect_scene_lighting_for_layer(1);
let visits = context.lighting_mask_node_visits.get();
context.set_scroller_surface_state(
nodes[1].id.to_u32(),
ScrollerSurfaceState {
scroll_y: 300.0,
presentation_scroll_y: 300.0,
..context
.get_scroller_surface_state(nodes[1].id.to_u32())
.unwrap()
},
);
let after = context.collect_scene_lighting_for_layer(1);
assert_eq!(
after.lights[0].position.y - before.lights[0].position.y,
300.0
);
assert_eq!(context.canvas_node_light_mask(surface.id), 1);
assert_eq!(context.lighting_mask_node_visits.get(), visits);
occlusion.render_layer_id = 2;
surface.occlusion.set(occlusion);
context.mark_occlusion_dirty();
context.collect_scene_lighting_for_layer(1);
assert!(context
.collect_scene_lighting_for_layer(2)
.lights
.is_empty());
assert_eq!(context.canvas_node_light_mask(surface.id), 0);
context.clear_all_dirty_canvases();
nodes[0].clone().recurse_unmount(&context);
assert!(context.is_canvas_dirty(&1));
}
#[test]
fn detached_source_changes_preserve_incremental_scene_reconciliation() {
let (context, root) = mount_test_tree(vec![TestLightingNode::new(
TestLightingRole::Surface,
Vec::new(),
)
.as_instance()]);
let owner = root.children.get()[0].clone();
let sources = owner.create_children_detached(
vec![(
TestLightingNode::new(TestLightingRole::Surface, Vec::new()).as_instance(),
owner.stack.clone(),
)],
&context,
&Rc::downgrade(&owner),
);
let source = owner
.attach_sidecar_children(sources, &context, &owner.parent_frame)
.remove(0);
source.mount_as_render_source(&owner, &context);
root.recurse_update(&context);
crate::engine::occlusion::update_node_occlusion(&root, &context);
let before = context.occlusion_stats();
source
.get_common_properties()
.borrow()
.x
.set(Some(pax_runtime_api::Size::Pixels(15.0.into())));
root.recurse_update(&context);
crate::engine::occlusion::update_node_occlusion(&root, &context);
let after = context.occlusion_stats();
assert_eq!(after.rebuilds, before.rebuilds);
assert!(after.records_updated > before.records_updated);
root.recurse_unmount(&context);
}
#[test]
fn canvas_surface_transform_uses_the_layer_owner_not_intermediate_frames() {
let leaf = TestLightingNode::new(TestLightingRole::Surface, Vec::new());
let mask = TestLightingNode::new(TestLightingRole::Frame, vec![leaf.as_instance()]);
let inner = TestLightingNode::new(TestLightingRole::Frame, vec![mask.as_instance()]);
let outer = TestLightingNode::new(TestLightingRole::Frame, vec![inner.as_instance()]);
let (context, root) = mount_test_tree(vec![outer.as_instance()]);
let outer = root.children.get()[0].clone();
let inner = outer.children.get()[0].clone();
let mask = inner.children.get()[0].clone();
let leaf = mask.children.get()[0].clone();
let outer_transform = Affine::translate((40.0, 70.0)) * Affine::scale(1.5);
let inner_local = Affine::translate((25.0, 35.0)) * Affine::rotate(0.3);
let inner_transform = outer_transform * inner_local;
let mask_transform = inner_transform * Affine::translate((60.0, 90.0));
let leaf_local = Affine::translate((8.0, 12.0));
let leaf_transform = mask_transform * leaf_local;
for (node, transform, layer, parent_frame) in [
(&outer, outer_transform, 0, None),
(&inner, inner_transform, 1, Some(outer.id)),
(&mask, mask_transform, 0, Some(inner.id)),
(&leaf, leaf_transform, 2, Some(mask.id)),
] {
node.transform_and_bounds
.replace_with(LocalProperty::new(TransformAndBounds {
transform: Transform2::new(transform.as_coeffs()),
bounds: (100.0, 100.0),
}));
let mut occlusion = node.occlusion.get();
occlusion.render_layer_id = layer;
node.occlusion.set(occlusion);
node.parent_frame
.replace_with(LocalProperty::new(parent_frame));
}
context.register_layer_scroller_owner(1, outer.id);
context.register_layer_scroller_owner(2, inner.id);
let assert_transform = |actual: Affine, expected: Affine| {
for (actual, expected) in actual.as_coeffs().into_iter().zip(expected.as_coeffs()) {
assert!((actual - expected).abs() < 1e-9, "{actual} != {expected}");
}
};
assert_transform(
context.canvas_surface_transform_for_node(&leaf),
Affine::translate((60.0, 90.0)) * leaf_local,
);
assert_transform(context.layer_to_root_transform(2), inner_transform);
context.clear_layer_scroller_owners();
assert_transform(
context.canvas_surface_transform_for_node(&leaf),
leaf_transform,
);
}
#[test]
fn touch_capture_releases_and_clears_removed_targets() {
let surface = TestLightingNode::new(TestLightingRole::Surface, Vec::new());
let (context, root) = mount_test_tree(vec![surface.as_instance()]);
let surface_node = root.children.get()[0].clone();
context.capture_touch_target(41, surface_node.id);
assert!(Rc::ptr_eq(
&context.captured_touch_target(41).unwrap(),
&surface_node
));
assert!(Rc::ptr_eq(
&context.release_touch_target(41).unwrap(),
&surface_node
));
assert!(context.captured_touch_target(41).is_none());
context.capture_touch_target(42, surface_node.id);
context.remove_from_cache(&surface_node);
assert!(context.captured_touch_target(42).is_none());
assert!(context.release_touch_target(42).is_none());
}
}