Skip to main content

pax_runtime/
cartridge.rs

1use_RefCell!();
2use crate::api::NodeContext;
3use crate::{
4    ConditionalProperties, ExpandedNode, Handler, HandlerRegistry, InstanceNode, InstantiationArgs,
5    ReusableInstanceNodeArgs, RouteLocation, RouteMatch, RuntimePropertiesStackFrame,
6    RuntimeResolvedPropertyColumns, RuntimeResolvedPropertyEntry, RuntimeSettingsCondition,
7    RuntimeSettingsLayer, RuntimeSettingsSource, INTERNAL_ROUTE_LOCATION_SYMBOL,
8    INTERNAL_ROUTE_MATCH_SYMBOL,
9};
10use pax_language::Computable;
11use pax_manifest::cartridge_generation::{
12    TRANSITION_GENERATION_SYMBOL, TRANSITION_PHASE_ENTER, TRANSITION_PHASE_EXIT,
13    TRANSITION_PHASE_SYMBOL, TRANSITION_PLAYHEAD_MILLIS_SYMBOL, TRANSITION_PLAYHEAD_SYMBOL,
14    TRANSITION_TAKEOVER_SYMBOL,
15};
16use pax_manifest::{
17    ExpressionInfo, GradientDefinition, GradientElement, GradientShapeDefinition,
18    InOutInterruption, LiteralBlockDefinition, LocationInfo, PaxIdentifier, SelectorExpr,
19    SettingElement, SettingsBlockElement, TemplateNodeDefinition, TimelineBlockElement,
20    TimelineDefinition, TimelineKeyframe, TimelineMarker, TimelineSelectorElement,
21    TimelineTrackDefinition, TimelineTrackElement, TransitionDefinition, TypeId, ValueDefinition,
22};
23use pax_message::{borrow, borrow_mut};
24use pax_runtime_api::constants::{COMMON_PROPERTIES, COMMON_PROPERTIES_2D};
25use pax_runtime_api::pax_value::functions::{call_function, Functions};
26use pax_runtime_api::pax_value::{CoercionRules, PaxAny, ToFromPaxAny, ToPaxValue};
27use pax_runtime_api::properties::{PropertyValue, UntypedProperty};
28use pax_runtime_api::{
29    use_RefCell, CommonProperties, Duration, EasingCurve, LocalProperty, Numeric, PaxValue,
30    Rotation, Size, Variable,
31};
32use std::any::Any;
33use std::borrow::Borrow;
34use std::collections::{BTreeMap, HashMap};
35use std::marker::PhantomData;
36use std::rc::Rc;
37
38pub trait PaxCartridge {}
39
40#[cfg(test)]
41mod initialization_tests;
42#[cfg(test)]
43mod typed_binding_tests;
44
45/// PAXEL symbol bound while resolving a property layer to the value from the
46/// preceding layer in that same property's precedence stack.
47pub const BASE_SYMBOL: &str = "$base";
48
49struct ResolvedRuntimeSettings {
50    #[allow(dead_code)]
51    defined_properties: BTreeMap<String, ValueDefinition>,
52    columns: RuntimeResolvedPropertyColumns,
53    provenance: BTreeMap<String, RuntimeResolvedPropertyEntry>,
54}
55
56fn imported_settings_layers_for_node(
57    expanded_node: Option<&Rc<ExpandedNode>>,
58) -> Vec<RuntimeSettingsLayer> {
59    let mut layers = Vec::new();
60    let Some(expanded_node) = expanded_node else {
61        return layers;
62    };
63    if expanded_node.is_in_import_settings_subtree() {
64        return layers;
65    }
66    let Some(containing_component) = expanded_node.containing_component.upgrade() else {
67        return layers;
68    };
69    layers.extend(
70        borrow!(containing_component.imported_settings_layers)
71            .iter()
72            .cloned(),
73    );
74    layers
75}
76
77fn append_resolved_property_entry(
78    columns: &mut RuntimeResolvedPropertyColumns,
79    key: &str,
80    entry: RuntimeResolvedPropertyEntry,
81) {
82    columns.entry(key.to_string()).or_default().push(entry);
83}
84
85fn add_removed_property_resets(
86    previous: &RuntimeResolvedPropertyColumns,
87    current: &mut RuntimeResolvedPropertyColumns,
88) {
89    for property_name in previous.keys() {
90        current.entry(property_name.clone()).or_default();
91    }
92}
93
94fn runtime_property_entry(
95    source: RuntimeSettingsSource,
96    selector: Option<pax_manifest::SelectorExpr>,
97    source_location: Option<LocationInfo>,
98    source_stack: Option<Rc<RuntimePropertiesStackFrame>>,
99    value: ValueDefinition,
100    condition: Option<RuntimeSettingsCondition>,
101    axis_index: Option<usize>,
102) -> RuntimeResolvedPropertyEntry {
103    RuntimeResolvedPropertyEntry {
104        transition: None,
105        source,
106        selector,
107        source_location,
108        source_stack,
109        value,
110        condition,
111        axis_index,
112    }
113}
114
115fn common_property_2d_axes(name: &str) -> Option<[&'static str; 2]> {
116    COMMON_PROPERTIES_2D
117        .iter()
118        .find_map(|(group_name, axes)| (*group_name == name).then_some(*axes))
119}
120
121fn setting_value_can_define_property(value: &ValueDefinition) -> bool {
122    matches!(
123        value,
124        ValueDefinition::LiteralValue(_)
125            | ValueDefinition::List(_)
126            | ValueDefinition::Block(_)
127            | ValueDefinition::Timeline(_)
128            | ValueDefinition::Gradient(_)
129            | ValueDefinition::Transition(_)
130            | ValueDefinition::Expression(_)
131            | ValueDefinition::Identifier(_)
132            | ValueDefinition::DoubleBinding(_)
133    )
134}
135
136fn is_lifecycle_transition_setting_key(key: &str) -> bool {
137    matches!(key, "in" | "out")
138}
139
140fn append_2d_common_property_entries(
141    columns: &mut RuntimeResolvedPropertyColumns,
142    key: &str,
143    value: &ValueDefinition,
144    source: RuntimeSettingsSource,
145    selector: Option<pax_manifest::SelectorExpr>,
146    source_location: Option<LocationInfo>,
147    source_stack: Option<Rc<RuntimePropertiesStackFrame>>,
148    condition: Option<RuntimeSettingsCondition>,
149) -> bool {
150    let Some(axes) = common_property_2d_axes(key) else {
151        return false;
152    };
153
154    append_resolved_property_entry(
155        columns,
156        key,
157        runtime_property_entry(
158            source.clone(),
159            selector.clone(),
160            source_location.clone(),
161            source_stack.clone(),
162            value.clone(),
163            condition.clone(),
164            None,
165        ),
166    );
167    for (axis_index, axis_name) in axes.iter().enumerate() {
168        append_resolved_property_entry(
169            columns,
170            axis_name,
171            runtime_property_entry(
172                source.clone(),
173                selector.clone(),
174                source_location.clone(),
175                source_stack.clone(),
176                value.clone(),
177                condition.clone(),
178                Some(axis_index),
179            ),
180        );
181    }
182    true
183}
184
185/// Converts a flattened property map into one-entry columns for legacy callers
186/// that do not participate in selector/import precedence layering.
187pub fn property_columns_from_defined_properties(
188    defined_properties: &BTreeMap<String, ValueDefinition>,
189) -> RuntimeResolvedPropertyColumns {
190    let mut columns = BTreeMap::new();
191    for (key, value) in defined_properties {
192        if setting_value_can_define_property(value) {
193            append_2d_common_property_entries(
194                &mut columns,
195                key,
196                value,
197                RuntimeSettingsSource::Inline,
198                None,
199                None,
200                None,
201                None,
202            );
203        }
204    }
205    for (key, value) in defined_properties {
206        if common_property_2d_axes(key).is_some() || !setting_value_can_define_property(value) {
207            continue;
208        }
209        append_resolved_property_entry(
210            &mut columns,
211            key,
212            runtime_property_entry(
213                RuntimeSettingsSource::Inline,
214                None,
215                None,
216                None,
217                value.clone(),
218                None,
219                None,
220            ),
221        );
222    }
223    columns
224}
225
226fn append_setting_elements(
227    columns: &mut RuntimeResolvedPropertyColumns,
228    elements: &[SettingElement],
229    source: RuntimeSettingsSource,
230    selector: Option<pax_manifest::SelectorExpr>,
231    source_stack: Option<Rc<RuntimePropertiesStackFrame>>,
232    condition: Option<RuntimeSettingsCondition>,
233) {
234    for element in elements {
235        let SettingElement::Setting(key, value) = element else {
236            continue;
237        };
238        if is_lifecycle_transition_setting_key(&key.token_value) {
239            continue;
240        }
241        if setting_value_can_define_property(value) {
242            append_2d_common_property_entries(
243                columns,
244                &key.token_value,
245                value,
246                source.clone(),
247                selector.clone(),
248                key.token_location.clone(),
249                source_stack.clone(),
250                condition.clone(),
251            );
252        }
253    }
254
255    for element in elements {
256        let SettingElement::Setting(key, value) = element else {
257            continue;
258        };
259        if is_lifecycle_transition_setting_key(&key.token_value) {
260            continue;
261        }
262        if common_property_2d_axes(&key.token_value).is_some()
263            || !setting_value_can_define_property(value)
264        {
265            continue;
266        }
267        append_resolved_property_entry(
268            columns,
269            &key.token_value,
270            runtime_property_entry(
271                source.clone(),
272                selector.clone(),
273                key.token_location.clone(),
274                source_stack.clone(),
275                value.clone(),
276                condition.clone(),
277                None,
278            ),
279        );
280    }
281}
282
283#[derive(Clone)]
284struct SelectorLayerBlock {
285    selector: String,
286    elements: Vec<SettingElement>,
287    condition: Option<RuntimeSettingsCondition>,
288}
289
290fn combine_settings_conditions(
291    parent: Option<&RuntimeSettingsCondition>,
292    branch_condition: Option<&ExpressionInfo>,
293    prior_branch_conditions: &[ExpressionInfo],
294) -> Option<RuntimeSettingsCondition> {
295    let mut condition = parent.cloned().unwrap_or_default();
296    condition
297        .negative
298        .extend(prior_branch_conditions.iter().cloned());
299    if let Some(branch_condition) = branch_condition {
300        condition.positive.push(branch_condition.clone());
301    }
302
303    if condition.positive.is_empty() && condition.negative.is_empty() {
304        None
305    } else {
306        Some(condition)
307    }
308}
309
310fn collect_selector_layer_blocks(
311    settings: &[SettingsBlockElement],
312    condition: Option<&RuntimeSettingsCondition>,
313    blocks: &mut Vec<SelectorLayerBlock>,
314) {
315    for setting in settings {
316        match setting {
317            SettingsBlockElement::SelectorBlock(token, block) => {
318                blocks.push(SelectorLayerBlock {
319                    selector: token.token_value.clone(),
320                    elements: block.elements.clone(),
321                    condition: condition.cloned(),
322                });
323            }
324            SettingsBlockElement::Conditional(block) => {
325                let mut prior_branch_conditions = Vec::new();
326                for branch in &block.branches {
327                    let branch_condition = combine_settings_conditions(
328                        condition,
329                        branch.condition_expression.as_ref(),
330                        &prior_branch_conditions,
331                    );
332                    collect_selector_layer_blocks(
333                        &branch.elements,
334                        branch_condition.as_ref(),
335                        blocks,
336                    );
337                    if let Some(condition_expression) = &branch.condition_expression {
338                        prior_branch_conditions.push(condition_expression.clone());
339                    }
340                }
341            }
342            SettingsBlockElement::Handler(_, _)
343            | SettingsBlockElement::Transition(_, _)
344            | SettingsBlockElement::Comment(_) => {}
345        }
346    }
347}
348
349fn append_selector_layer_entries(
350    columns: &mut RuntimeResolvedPropertyColumns,
351    tnd: &TemplateNodeDefinition,
352    classes: &[String],
353    settings_block: &Option<Vec<SettingsBlockElement>>,
354    source: RuntimeSettingsSource,
355    source_stack: Option<Rc<RuntimePropertiesStackFrame>>,
356) {
357    let Some(settings_block) = settings_block else {
358        return;
359    };
360
361    let mut selector_blocks = Vec::new();
362    collect_selector_layer_blocks(settings_block, None, &mut selector_blocks);
363
364    for settings_value in &selector_blocks {
365        let Ok(selector) = pax_manifest::SelectorExpr::parse(&settings_value.selector) else {
366            continue;
367        };
368        if matches!(selector, pax_manifest::SelectorExpr::Type(_))
369            && tnd.selector_info.matches(&tnd.type_id, &selector)
370        {
371            append_setting_elements(
372                columns,
373                &settings_value.elements,
374                source.clone(),
375                Some(selector),
376                source_stack.clone(),
377                settings_value.condition.clone(),
378            );
379        }
380    }
381
382    for class in classes {
383        let selector = pax_manifest::SelectorExpr::Class(class.clone());
384        for settings_value in &selector_blocks {
385            let Ok(candidate) = pax_manifest::SelectorExpr::parse(&settings_value.selector) else {
386                continue;
387            };
388            if candidate == selector {
389                append_setting_elements(
390                    columns,
391                    &settings_value.elements,
392                    source.clone(),
393                    Some(selector.clone()),
394                    source_stack.clone(),
395                    settings_value.condition.clone(),
396                );
397            }
398        }
399    }
400    if let Some(id) = &tnd.selector_info.id {
401        let selector = pax_manifest::SelectorExpr::Id(id.token_value.clone());
402        for settings_value in &selector_blocks {
403            let Ok(candidate) = pax_manifest::SelectorExpr::parse(&settings_value.selector) else {
404                continue;
405            };
406            if candidate == selector {
407                append_setting_elements(
408                    columns,
409                    &settings_value.elements,
410                    source.clone(),
411                    Some(selector.clone()),
412                    source_stack.clone(),
413                    settings_value.condition.clone(),
414                );
415            }
416        }
417    }
418}
419
420fn overlay_static_runtime_properties(
421    columns: &mut RuntimeResolvedPropertyColumns,
422    base_defined_properties: &BTreeMap<String, ValueDefinition>,
423) {
424    for (key, value) in base_defined_properties {
425        if matches!(
426            value,
427            ValueDefinition::Timeline(_) | ValueDefinition::Transition(_)
428        ) {
429            if append_2d_common_property_entries(
430                columns,
431                key,
432                value,
433                RuntimeSettingsSource::Inline,
434                None,
435                None,
436                None,
437                None,
438            ) {
439                continue;
440            }
441            append_resolved_property_entry(
442                columns,
443                key,
444                runtime_property_entry(
445                    RuntimeSettingsSource::Inline,
446                    None,
447                    None,
448                    None,
449                    value.clone(),
450                    None,
451                    None,
452                ),
453            );
454        }
455    }
456}
457
458fn timeline_transition_kind(
459    timeline: &TimelineDefinition,
460    settings: &Option<Vec<SettingsBlockElement>>,
461) -> Option<u64> {
462    let timeline_name = timeline.name.as_ref()?.token_value.as_str();
463    settings.as_ref()?.iter().find_map(|setting| {
464        let SettingsBlockElement::Transition(kind, name) = setting else {
465            return None;
466        };
467        if name.token_value != timeline_name {
468            return None;
469        }
470        match kind.token_value.as_str() {
471            "in" => Some(TRANSITION_PHASE_ENTER),
472            "out" => Some(TRANSITION_PHASE_EXIT),
473            _ => None,
474        }
475    })
476}
477
478fn timeline_target_matches_node(
479    target: &str,
480    tnd: &TemplateNodeDefinition,
481    classes: &[String],
482) -> bool {
483    if let Some(class_name) = target.strip_prefix('.') {
484        return classes.iter().any(|candidate| candidate == class_name);
485    }
486    if let Some(id) = target.strip_prefix('#') {
487        return tnd
488            .selector_info
489            .id
490            .as_ref()
491            .map(|candidate| candidate.token_value == id)
492            .unwrap_or(false);
493    }
494    false
495}
496
497fn append_timeline_selector_entries(
498    columns: &mut RuntimeResolvedPropertyColumns,
499    tnd: &TemplateNodeDefinition,
500    timelines: &[TimelineDefinition],
501    component_settings: &Option<Vec<SettingsBlockElement>>,
502    classes: &[String],
503    transition_classes: Option<&[String]>,
504) {
505    let mut values =
506        BTreeMap::<String, (SelectorExpr, Option<LocationInfo>, ValueDefinition)>::new();
507    let empty_base = BTreeMap::new();
508
509    for timeline in timelines {
510        let transition_kind = timeline_transition_kind(timeline, component_settings);
511        let selector_classes = transition_kind.and(transition_classes).unwrap_or(classes);
512        for element in &timeline.elements {
513            let TimelineBlockElement::SelectorBlock(target, block) = element else {
514                continue;
515            };
516            if !timeline_target_matches_node(&target.token_value, tnd, selector_classes) {
517                continue;
518            }
519            let Ok(selector) = SelectorExpr::parse(&target.token_value) else {
520                continue;
521            };
522            for element in &block.elements {
523                let TimelineSelectorElement::Track(property, track) = element else {
524                    continue;
525                };
526                let value = if let Some(transition_kind) = transition_kind {
527                    let track = pax_manifest::PaxManifest::prepare_transition_track(
528                        timeline,
529                        track,
530                        &empty_base,
531                        &property.token_value,
532                    );
533                    let existing = values
534                        .remove(&property.token_value)
535                        .map(|(_, _, value)| value);
536                    let mut transition = match existing {
537                        Some(ValueDefinition::Transition(transition)) => transition,
538                        Some(value) => TransitionDefinition {
539                            starting_value: Some(Box::new(value)),
540                            ..Default::default()
541                        },
542                        None => TransitionDefinition::default(),
543                    };
544                    match transition_kind {
545                        TRANSITION_PHASE_ENTER => transition.enter = Some(track),
546                        TRANSITION_PHASE_EXIT => transition.exit = Some(track),
547                        _ => {}
548                    }
549                    ValueDefinition::Transition(transition)
550                } else {
551                    ValueDefinition::Timeline(pax_manifest::PaxManifest::prepare_timeline_track(
552                        timeline, track,
553                    ))
554                };
555                values.insert(
556                    property.token_value.clone(),
557                    (selector.clone(), property.token_location.clone(), value),
558                );
559            }
560        }
561    }
562
563    for (property, (selector, source_location, value)) in values {
564        append_resolved_property_entry(
565            columns,
566            &property,
567            runtime_property_entry(
568                RuntimeSettingsSource::ComponentSettings,
569                Some(selector),
570                source_location,
571                None,
572                value,
573                None,
574                None,
575            ),
576        );
577    }
578}
579
580fn resolve_runtime_settings_with_layers_for_node(
581    tnd: &TemplateNodeDefinition,
582    base_defined_properties: &BTreeMap<String, ValueDefinition>,
583    containing_component_settings: &Option<Vec<SettingsBlockElement>>,
584    imported_layers: &[RuntimeSettingsLayer],
585    classes: &[String],
586    transition_classes: Option<&[String]>,
587    timelines: &[TimelineDefinition],
588) -> ResolvedRuntimeSettings {
589    let mut columns = BTreeMap::new();
590
591    append_selector_layer_entries(
592        &mut columns,
593        tnd,
594        classes,
595        containing_component_settings,
596        RuntimeSettingsSource::ComponentSettings,
597        None,
598    );
599    for layer in imported_layers {
600        let mut imported_columns = BTreeMap::new();
601        append_selector_layer_entries(
602            &mut imported_columns,
603            tnd,
604            classes,
605            &Some(layer.settings.clone()),
606            RuntimeSettingsSource::ImportedLayer {
607                provider_id: layer.provider_id,
608                provider_type_id: layer.provider_type_id.clone(),
609            },
610            Some(Rc::clone(&layer.provider_stack)),
611        );
612        for (name, mut entries) in imported_columns {
613            for entry in &mut entries {
614                entry.transition = layer.transition.clone();
615            }
616            columns.entry(name).or_default().extend(entries);
617        }
618    }
619    append_timeline_selector_entries(
620        &mut columns,
621        tnd,
622        timelines,
623        containing_component_settings,
624        classes,
625        transition_classes,
626    );
627    if let Some(inline_settings) = &tnd.settings {
628        append_setting_elements(
629            &mut columns,
630            inline_settings,
631            RuntimeSettingsSource::Inline,
632            None,
633            None,
634            None,
635        );
636    }
637    overlay_static_runtime_properties(&mut columns, base_defined_properties);
638
639    let mut defined_properties = BTreeMap::new();
640    let mut provenance = BTreeMap::new();
641    for (key, entries) in &columns {
642        let Some(last_entry) = entries.last() else {
643            continue;
644        };
645        defined_properties.insert(key.clone(), last_entry.value.clone());
646        provenance.insert(key.clone(), last_entry.clone());
647    }
648
649    ResolvedRuntimeSettings {
650        defined_properties,
651        columns,
652        provenance,
653    }
654}
655
656fn resolve_runtime_settings_for_node(
657    tnd: &TemplateNodeDefinition,
658    base_defined_properties: &BTreeMap<String, ValueDefinition>,
659    containing_component_settings: &Option<Vec<SettingsBlockElement>>,
660    timelines: &[TimelineDefinition],
661    expanded_node: Option<&Rc<ExpandedNode>>,
662) -> ResolvedRuntimeSettings {
663    let imported_layers = imported_settings_layers_for_node(expanded_node);
664    let classes = expanded_node
665        .map(|node| node.selector_metadata.borrow().classes.get())
666        .unwrap_or_else(|| tnd.selector_info.literal_classes());
667    let transition_classes =
668        expanded_node.and_then(|node| node.transition_selector_classes.borrow().clone());
669    resolve_runtime_settings_with_layers_for_node(
670        tnd,
671        base_defined_properties,
672        containing_component_settings,
673        &imported_layers,
674        &classes,
675        transition_classes.as_deref(),
676        timelines,
677    )
678}
679
680#[allow(dead_code)]
681fn resolve_defined_properties_for_node(
682    tnd: &TemplateNodeDefinition,
683    base_defined_properties: &BTreeMap<String, ValueDefinition>,
684    containing_component_settings: &Option<Vec<SettingsBlockElement>>,
685    timelines: &[TimelineDefinition],
686    expanded_node: Option<&Rc<ExpandedNode>>,
687) -> BTreeMap<String, ValueDefinition> {
688    let resolved = resolve_runtime_settings_for_node(
689        tnd,
690        base_defined_properties,
691        containing_component_settings,
692        timelines,
693        expanded_node,
694    );
695    if let Some(expanded_node) = expanded_node {
696        *expanded_node.resolved_property_columns.borrow_mut() = resolved.columns.clone();
697        *expanded_node.resolved_property_provenance.borrow_mut() = resolved.provenance.clone();
698    }
699    resolved.defined_properties
700}
701
702fn resolve_property_columns_for_node(
703    tnd: &TemplateNodeDefinition,
704    base_defined_properties: &BTreeMap<String, ValueDefinition>,
705    containing_component_settings: &Option<Vec<SettingsBlockElement>>,
706    timelines: &[TimelineDefinition],
707    expanded_node: Option<&Rc<ExpandedNode>>,
708) -> RuntimeResolvedPropertyColumns {
709    let mut resolved = resolve_runtime_settings_for_node(
710        tnd,
711        base_defined_properties,
712        containing_component_settings,
713        timelines,
714        expanded_node,
715    );
716    if let Some(expanded_node) = expanded_node {
717        if expanded_node.reset_removed_runtime_properties.get() {
718            add_removed_property_resets(
719                &expanded_node.resolved_property_columns.borrow(),
720                &mut resolved.columns,
721            );
722        }
723        *expanded_node.resolved_property_columns.borrow_mut() = resolved.columns.clone();
724        *expanded_node.resolved_property_provenance.borrow_mut() = resolved.provenance.clone();
725    }
726    resolved.columns
727}
728
729/// Immutable template inputs shared by common and component property binding.
730/// Resolved columns and reactive properties are always node-local; this plan
731/// never caches evaluated values or selector matches across nodes or rebinds.
732pub struct TemplatePropertyPlan {
733    node: TemplateNodeDefinition,
734    base_defined_properties: BTreeMap<String, ValueDefinition>,
735    component_settings: Option<Vec<SettingsBlockElement>>,
736    timelines: Vec<TimelineDefinition>,
737    pub(crate) requires_pre_node_binding: bool,
738    #[cfg(test)]
739    resolutions: std::cell::Cell<usize>,
740}
741
742impl TemplatePropertyPlan {
743    pub(crate) fn new(
744        node: TemplateNodeDefinition,
745        base_defined_properties: BTreeMap<String, ValueDefinition>,
746        component_settings: Option<Vec<SettingsBlockElement>>,
747        timelines: Vec<TimelineDefinition>,
748    ) -> Self {
749        let requires_pre_node_binding = base_defined_properties
750            .values()
751            .any(value_uses_local_property_scope)
752            || node.settings.iter().flatten().any(|setting| {
753                matches!(setting,
754                SettingElement::Setting(_, value) if value_uses_local_property_scope(value))
755            });
756        Self {
757            node,
758            base_defined_properties,
759            component_settings,
760            timelines,
761            requires_pre_node_binding,
762            #[cfg(test)]
763            resolutions: std::cell::Cell::new(0),
764        }
765    }
766
767    pub(crate) fn resolve(
768        &self,
769        node: Option<&Rc<ExpandedNode>>,
770    ) -> RuntimeResolvedPropertyColumns {
771        #[cfg(test)]
772        self.resolutions.set(self.resolutions.get() + 1);
773        resolve_property_columns_for_node(
774            &self.node,
775            &self.base_defined_properties,
776            &self.component_settings,
777            &self.timelines,
778            node,
779        )
780    }
781}
782
783pub(crate) fn value_uses_local_property_scope(value: &ValueDefinition) -> bool {
784    match value {
785        ValueDefinition::List(values) => values.iter().any(value_uses_local_property_scope),
786        ValueDefinition::Block(block) => block.elements.iter().any(|setting| matches!(setting, SettingElement::Setting(_, value) if value_uses_local_property_scope(value))),
787        ValueDefinition::Timeline(track) => track.use_local_property_scope,
788        ValueDefinition::Transition(transition) => transition
789            .enter
790            .iter()
791            .chain(transition.exit.iter())
792            .any(|track| track.use_local_property_scope),
793        _ => false,
794    }
795}
796
797pub struct HandlerDescriptor {
798    pub name: &'static str,
799    pub function: fn(Rc<RefCell<PaxAny>>, &NodeContext, Option<PaxAny>),
800}
801
802impl HandlerDescriptor {
803    pub const fn new(
804        name: &'static str,
805        function: fn(Rc<RefCell<PaxAny>>, &NodeContext, Option<PaxAny>),
806    ) -> Self {
807        Self { name, function }
808    }
809}
810
811pub struct PropertyScopeDescriptor<T> {
812    pub name: &'static str,
813    pub variable: fn(&T) -> Variable,
814    marker: PhantomData<fn(&T)>,
815}
816
817impl<T> PropertyScopeDescriptor<T> {
818    pub const fn new(name: &'static str, variable: fn(&T) -> Variable) -> Self {
819        Self {
820            name,
821            variable,
822            marker: PhantomData,
823        }
824    }
825}
826
827/// Runtime descriptor for applying all resolved layers of a generated component
828/// property.
829pub struct ComponentPropertyDescriptor<T> {
830    /// LocalProperty name as it appears in Pax templates and settings.
831    pub name: &'static str,
832    /// Generated applicator for all resolved layers of this property.
833    pub apply_entries: fn(
834        &mut T,
835        &[RuntimeResolvedPropertyEntry],
836        &Rc<RuntimePropertiesStackFrame>,
837        Option<&ExpandedNode>,
838    ),
839    marker: PhantomData<fn(&T)>,
840}
841
842impl<T> ComponentPropertyDescriptor<T> {
843    /// Creates a descriptor for a generated component property.
844    pub const fn new(
845        name: &'static str,
846        apply_entries: fn(
847            &mut T,
848            &[RuntimeResolvedPropertyEntry],
849            &Rc<RuntimePropertiesStackFrame>,
850            Option<&ExpandedNode>,
851        ),
852    ) -> Self {
853        Self {
854            name,
855            apply_entries,
856            marker: PhantomData,
857        }
858    }
859}
860
861pub struct ComponentDescriptor<T: Default + ToFromPaxAny + 'static> {
862    pub type_id: &'static str,
863    pub property_scope_descriptors: &'static [PropertyScopeDescriptor<T>],
864    pub property_descriptors: &'static [ComponentPropertyDescriptor<T>],
865    pub handler_descriptors: &'static [HandlerDescriptor],
866    pub instantiate: fn(InstantiationArgs) -> Rc<dyn InstanceNode>,
867    marker: PhantomData<fn(&T)>,
868}
869
870impl<T: Default + ToFromPaxAny + 'static> ComponentDescriptor<T> {
871    pub const fn new(
872        type_id: &'static str,
873        property_scope_descriptors: &'static [PropertyScopeDescriptor<T>],
874        property_descriptors: &'static [ComponentPropertyDescriptor<T>],
875        handler_descriptors: &'static [HandlerDescriptor],
876        instantiate: fn(InstantiationArgs) -> Rc<dyn InstanceNode>,
877    ) -> Self {
878        Self {
879            type_id,
880            property_scope_descriptors,
881            property_descriptors,
882            handler_descriptors,
883            instantiate,
884            marker: PhantomData,
885        }
886    }
887}
888
889pub struct ErasedComponentDescriptor {
890    pub type_id: &'static str,
891    pub typed_descriptor: &'static (dyn Any + Sync),
892    pub create_properties: fn() -> PaxAny,
893    pub apply_defined_properties: fn(
894        &'static (dyn Any + Sync),
895        &mut PaxAny,
896        &RuntimeResolvedPropertyColumns,
897        &Rc<RuntimePropertiesStackFrame>,
898        Option<&ExpandedNode>,
899    ),
900    pub build_property_scope: fn(&'static (dyn Any + Sync), &PaxAny) -> HashMap<String, Variable>,
901    pub handler_descriptors: &'static [HandlerDescriptor],
902    pub instantiate: fn(InstantiationArgs) -> Rc<dyn InstanceNode>,
903}
904
905impl ErasedComponentDescriptor {
906    pub const fn new(
907        type_id: &'static str,
908        typed_descriptor: &'static (dyn Any + Sync),
909        create_properties: fn() -> PaxAny,
910        apply_defined_properties: fn(
911            &'static (dyn Any + Sync),
912            &mut PaxAny,
913            &RuntimeResolvedPropertyColumns,
914            &Rc<RuntimePropertiesStackFrame>,
915            Option<&ExpandedNode>,
916        ),
917        build_property_scope: fn(&'static (dyn Any + Sync), &PaxAny) -> HashMap<String, Variable>,
918        handler_descriptors: &'static [HandlerDescriptor],
919        instantiate: fn(InstantiationArgs) -> Rc<dyn InstanceNode>,
920    ) -> Self {
921        Self {
922            type_id,
923            typed_descriptor,
924            create_properties,
925            apply_defined_properties,
926            build_property_scope,
927            handler_descriptors,
928            instantiate,
929        }
930    }
931}
932
933pub struct ComponentDescriptorRegistryEntry {
934    pub type_id: &'static str,
935    pub descriptor: &'static ErasedComponentDescriptor,
936}
937
938impl ComponentDescriptorRegistryEntry {
939    pub const fn new(
940        type_id: &'static str,
941        descriptor: &'static ErasedComponentDescriptor,
942    ) -> Self {
943        Self {
944            type_id,
945            descriptor,
946        }
947    }
948}
949
950pub struct TypeFieldDescriptor<T> {
951    pub name: &'static str,
952    pub apply: fn(&mut T, &ValueDefinition, &Rc<RuntimePropertiesStackFrame>),
953    pub dependency: Option<fn(&T) -> UntypedProperty>,
954    pub touch: Option<fn(&T)>,
955    marker: PhantomData<fn(&T)>,
956}
957
958impl<T> TypeFieldDescriptor<T> {
959    pub const fn new(
960        name: &'static str,
961        apply: fn(&mut T, &ValueDefinition, &Rc<RuntimePropertiesStackFrame>),
962    ) -> Self {
963        Self {
964            name,
965            apply,
966            dependency: None,
967            touch: None,
968            marker: PhantomData,
969        }
970    }
971
972    pub const fn with_property_dependency(
973        name: &'static str,
974        apply: fn(&mut T, &ValueDefinition, &Rc<RuntimePropertiesStackFrame>),
975        dependency: fn(&T) -> UntypedProperty,
976        touch: fn(&T),
977    ) -> Self {
978        Self {
979            name,
980            apply,
981            dependency: Some(dependency),
982            touch: Some(touch),
983            marker: PhantomData,
984        }
985    }
986}
987
988pub struct TypeDescriptor<T: 'static> {
989    pub name: &'static str,
990    pub fields: &'static [TypeFieldDescriptor<T>],
991    marker: PhantomData<fn(&T)>,
992}
993
994impl<T> TypeDescriptor<T> {
995    pub const fn new(name: &'static str, fields: &'static [TypeFieldDescriptor<T>]) -> Self {
996        Self {
997            name,
998            fields,
999            marker: PhantomData,
1000        }
1001    }
1002}
1003
1004fn missing_handler(_: Rc<RefCell<PaxAny>>, _: &NodeContext, _: Option<PaxAny>) {}
1005
1006pub fn resolve_handler(
1007    descriptors: &[HandlerDescriptor],
1008    fn_name: &str,
1009) -> fn(Rc<RefCell<PaxAny>>, &NodeContext, Option<PaxAny>) {
1010    descriptors
1011        .iter()
1012        .find(|descriptor| descriptor.name == fn_name)
1013        .map(|descriptor| descriptor.function)
1014        .unwrap_or_else(|| {
1015            log::warn!("Unknown handler name {}", fn_name);
1016            missing_handler
1017        })
1018}
1019
1020pub fn build_component_handler_registry(
1021    descriptors: &[HandlerDescriptor],
1022    handlers: Vec<(String, Vec<String>)>,
1023) -> Rc<RefCell<HandlerRegistry>> {
1024    let mut handler_registry = HandlerRegistry::default();
1025    for (event, functions) in handlers {
1026        handler_registry.handlers.insert(
1027            event,
1028            functions
1029                .iter()
1030                .map(|fn_name| {
1031                    Handler::new_component_handler(resolve_handler(descriptors, fn_name))
1032                })
1033                .collect(),
1034        );
1035    }
1036    Rc::new(RefCell::new(handler_registry))
1037}
1038
1039pub fn add_inline_handlers_from_descriptors(
1040    descriptors: &[HandlerDescriptor],
1041    handlers: Vec<(String, String)>,
1042    registry: Rc<RefCell<HandlerRegistry>>,
1043) -> Rc<RefCell<HandlerRegistry>> {
1044    {
1045        let mut registry_mut = borrow_mut!(registry);
1046        for (event, fn_name) in handlers {
1047            let handler_vec = registry_mut.handlers.entry(event).or_insert_with(Vec::new);
1048            handler_vec.push(Handler::new_inline_handler(resolve_handler(
1049                descriptors,
1050                &fn_name,
1051            )));
1052        }
1053    }
1054    registry
1055}
1056
1057pub fn build_property_scope<T>(
1058    properties: &T,
1059    descriptors: &[PropertyScopeDescriptor<T>],
1060) -> HashMap<String, Variable> {
1061    descriptors
1062        .iter()
1063        .map(|descriptor| {
1064            (
1065                descriptor.name.to_string(),
1066                (descriptor.variable)(properties),
1067            )
1068        })
1069        .collect()
1070}
1071
1072pub fn apply_component_descriptor_properties<T: Default + ToFromPaxAny + 'static>(
1073    properties: &mut T,
1074    descriptor: &'static ComponentDescriptor<T>,
1075    property_columns: &RuntimeResolvedPropertyColumns,
1076    stack_frame: &Rc<RuntimePropertiesStackFrame>,
1077    node: Option<&ExpandedNode>,
1078) {
1079    thread_local! {
1080        static WARNED_INCOMPATIBLE_SELECTOR_PROPERTIES: RefCell<std::collections::HashSet<String>> =
1081            RefCell::new(std::collections::HashSet::new());
1082    }
1083
1084    for (property_name, entries) in property_columns {
1085        let is_known = descriptor
1086            .property_descriptors
1087            .iter()
1088            .any(|property| property.name == property_name)
1089            || COMMON_PROPERTIES.contains(&property_name.as_str())
1090            || matches!(
1091                property_name.as_str(),
1092                "unclippable" | "_raycastable" | "_suspended"
1093            );
1094        if is_known {
1095            continue;
1096        }
1097        for entry in entries.iter().filter(|entry| entry.selector.is_some()) {
1098            let key = format!(
1099                "{}:{property_name}:{:?}",
1100                descriptor.type_id, entry.selector
1101            );
1102            WARNED_INCOMPATIBLE_SELECTOR_PROPERTIES.with(|warnings| {
1103                if warnings.borrow_mut().insert(key) {
1104                    let location = entry
1105                        .source_location
1106                        .as_ref()
1107                        .map(|location| {
1108                            format!(
1109                                " at {}:{}",
1110                                location.start_line_col.0, location.start_line_col.1
1111                            )
1112                        })
1113                        .unwrap_or_default();
1114                    log::warn!(
1115                        "selector {:?} assigns property '{}' which does not apply to {}{}; ignoring it",
1116                        entry.selector,
1117                        property_name,
1118                        descriptor.type_id,
1119                        location,
1120                    );
1121                }
1122            });
1123        }
1124    }
1125
1126    for property_descriptor in descriptor.property_descriptors {
1127        if let Some(entries) = property_columns.get(property_descriptor.name) {
1128            (property_descriptor.apply_entries)(properties, entries, stack_frame, node);
1129        }
1130    }
1131}
1132
1133pub fn resolve_component_descriptor(
1134    entries: &[ComponentDescriptorRegistryEntry],
1135    type_id: &TypeId,
1136) -> Option<&'static ErasedComponentDescriptor> {
1137    let identifier = type_id.get_unique_identifier();
1138    entries
1139        .iter()
1140        .find(|entry| entry.type_id == identifier)
1141        .map(|entry| entry.descriptor)
1142}
1143
1144pub fn erased_create_properties<T: Default + ToFromPaxAny + 'static>() -> PaxAny {
1145    T::default().to_pax_any()
1146}
1147
1148pub fn erased_apply_defined_properties<T: Default + ToFromPaxAny + 'static>(
1149    typed_descriptor: &'static (dyn Any + Sync),
1150    pax_any: &mut PaxAny,
1151    property_columns: &RuntimeResolvedPropertyColumns,
1152    stack_frame: &Rc<RuntimePropertiesStackFrame>,
1153    node: Option<&ExpandedNode>,
1154) {
1155    let descriptor = (typed_descriptor as &dyn Any)
1156        .downcast_ref::<ComponentDescriptor<T>>()
1157        .expect("Failed to downcast erased component descriptor");
1158    let properties = T::mut_from_pax_any(pax_any).unwrap_or_else(|err| {
1159        panic!(
1160            "Failed to downcast properties to {}: {}",
1161            descriptor.type_id, err
1162        )
1163    });
1164    apply_component_descriptor_properties(
1165        properties,
1166        descriptor,
1167        property_columns,
1168        stack_frame,
1169        node,
1170    );
1171}
1172
1173pub fn erased_build_property_scope<T: Default + ToFromPaxAny + 'static>(
1174    typed_descriptor: &'static (dyn Any + Sync),
1175    pax_any: &PaxAny,
1176) -> HashMap<String, Variable> {
1177    let descriptor = (typed_descriptor as &dyn Any)
1178        .downcast_ref::<ComponentDescriptor<T>>()
1179        .expect("Failed to downcast erased component descriptor");
1180    let properties = T::ref_from_pax_any(pax_any).unwrap_or_else(|err| {
1181        panic!(
1182            "Failed to downcast properties to {}: {}",
1183            descriptor.type_id, err
1184        )
1185    });
1186    build_property_scope(properties, descriptor.property_scope_descriptors)
1187}
1188
1189pub fn build_component_handler_registry_for_descriptor(
1190    descriptor: &'static ErasedComponentDescriptor,
1191    handlers: Vec<(String, Vec<String>)>,
1192) -> Rc<RefCell<HandlerRegistry>> {
1193    build_component_handler_registry(descriptor.handler_descriptors, handlers)
1194}
1195
1196pub fn add_inline_handlers_from_component_descriptor(
1197    descriptor: &'static ErasedComponentDescriptor,
1198    handlers: Vec<(String, String)>,
1199    registry: Rc<RefCell<HandlerRegistry>>,
1200) -> Rc<RefCell<HandlerRegistry>> {
1201    add_inline_handlers_from_descriptors(descriptor.handler_descriptors, handlers, registry)
1202}
1203
1204pub fn instantiate_component_from_descriptor(
1205    descriptor: &'static ErasedComponentDescriptor,
1206    args: InstantiationArgs,
1207) -> Rc<dyn InstanceNode> {
1208    (descriptor.instantiate)(args)
1209}
1210
1211pub fn build_literal_block_property<T: PropertyValue>(
1212    descriptor: &'static TypeDescriptor<T>,
1213    args: &LiteralBlockDefinition,
1214    stack_frame: Rc<RuntimePropertiesStackFrame>,
1215) -> LocalProperty<T> {
1216    if descriptor.fields.is_empty() {
1217        for setting in &args.elements {
1218            if let SettingElement::Setting(k, _) = setting {
1219                panic!("Unknown property name {}", k.token_value);
1220            }
1221        }
1222        return LocalProperty::new_with_name(Default::default(), descriptor.name);
1223    }
1224
1225    let mut properties = T::default();
1226    for setting in &args.elements {
1227        if let SettingElement::Setting(k, value_definition) = setting {
1228            let field_descriptor = descriptor
1229                .fields
1230                .iter()
1231                .find(|descriptor| descriptor.name == k.token_value)
1232                .unwrap_or_else(|| panic!("Unknown property name {}", k.token_value));
1233            (field_descriptor.apply)(&mut properties, value_definition, &stack_frame);
1234        }
1235    }
1236
1237    let dependents: Vec<_> = descriptor
1238        .fields
1239        .iter()
1240        .filter_map(|descriptor| {
1241            descriptor
1242                .dependency
1243                .map(|dependency| dependency(&properties))
1244        })
1245        .collect();
1246
1247    if dependents.is_empty() {
1248        return LocalProperty::new_with_name(properties, descriptor.name);
1249    }
1250
1251    let touchers: Vec<_> = descriptor
1252        .fields
1253        .iter()
1254        .filter_map(|descriptor| descriptor.touch)
1255        .collect();
1256    let computed_properties = properties.clone();
1257    LocalProperty::computed_with_name(
1258        move || {
1259            let cloned_properties = computed_properties.clone();
1260            for touch in &touchers {
1261                touch(&cloned_properties);
1262            }
1263            cloned_properties
1264        },
1265        &dependents,
1266        descriptor.name,
1267    )
1268}
1269
1270fn build_conditional_branch_property(
1271    branch_kind: pax_manifest::ControlFlowConditionalBranchKind,
1272    condition_expression: Option<ExpressionInfo>,
1273    stack_frame: Rc<RuntimePropertiesStackFrame>,
1274) -> LocalProperty<bool> {
1275    match condition_expression {
1276        Some(expr_info) => build_conditional_expression_property(expr_info, stack_frame),
1277        None => LocalProperty::new(matches!(
1278            branch_kind,
1279            pax_manifest::ControlFlowConditionalBranchKind::Else
1280        )),
1281    }
1282}
1283
1284fn build_conditional_expression_property(
1285    expr_info: ExpressionInfo,
1286    stack_frame: Rc<RuntimePropertiesStackFrame>,
1287) -> LocalProperty<bool> {
1288    let cloned_stack = stack_frame.clone();
1289    let expr_ast = expr_info.expression.clone();
1290
1291    let mut dependencies = Vec::new();
1292    for dependency in &expr_info.dependencies {
1293        if let Some(p) = stack_frame.resolve_symbol_as_erased_property(dependency) {
1294            dependencies.push(p);
1295        } else {
1296            log::warn!("Failed to resolve symbol {}", dependency);
1297        }
1298    }
1299
1300    let name = format!("conditional (if) expr ({})", expr_ast);
1301    LocalProperty::computed_with_name(
1302        move || {
1303            let new_value = expr_ast
1304                .compute(cloned_stack.clone())
1305                .unwrap_or_else(|err| {
1306                    log::warn!("Failed to compute expression: {:?}", err);
1307                    Default::default()
1308                });
1309            bool::try_coerce(new_value).unwrap_or_else(|_e| {
1310                log::warn!("Failed to parse boolean expression: {}", expr_ast);
1311                Default::default()
1312            })
1313        },
1314        &dependencies,
1315        &name,
1316    )
1317}
1318
1319pub trait DefinitionToInstanceTraverser {
1320    fn new(manifest: pax_manifest::PaxManifest) -> Self
1321    where
1322        Self: Sized;
1323
1324    fn get_manifest(&self) -> std::cell::Ref<'_, pax_manifest::PaxManifest>;
1325
1326    #[cfg(feature = "designtime")]
1327    fn get_designtime_manager(
1328        &self,
1329        project_query: String,
1330    ) -> std::option::Option<std::rc::Rc<RefCell<pax_designtime::DesigntimeManager>>>;
1331
1332    /// Adopt the initializer's allocation without converting or defaulting it.
1333    /// Component settings and timeline bindings still apply to the supplied values.
1334    fn get_main_component_with_properties(
1335        &self,
1336        id: &str,
1337        properties: Rc<RefCell<PaxAny>>,
1338    ) -> Rc<crate::ComponentInstance> {
1339        let main_component_type_id = self.get_manifest().main_component_type_id.clone();
1340        let mut args = self.build_component_args(&main_component_type_id);
1341        args.template_node_identifier = Some(pax_manifest::UniqueTemplateNodeIdentifier::build(
1342            TypeId::build_singleton(id, Some("RootComponent")),
1343            pax_manifest::TemplateNodeId::build(0),
1344        ));
1345        args.prototypical_properties = args.prototypical_properties.adopt(properties);
1346        crate::ComponentInstance::instantiate(args)
1347    }
1348
1349    fn get_main_component(&self, id: &str) -> std::rc::Rc<crate::ComponentInstance> {
1350        let main_component_type_id = {
1351            let manifest = self.get_manifest();
1352            manifest.main_component_type_id.clone()
1353        };
1354
1355        let wrapper_type_id = TypeId::build_singleton(id, Some("RootComponent"));
1356
1357        let mut args = self.build_component_args(&main_component_type_id);
1358        args.template_node_identifier = Some(pax_manifest::UniqueTemplateNodeIdentifier::build(
1359            wrapper_type_id,
1360            pax_manifest::TemplateNodeId::build(0),
1361        ));
1362        let main_component = crate::ComponentInstance::instantiate(args);
1363        main_component
1364    }
1365
1366    fn get_component(
1367        &mut self,
1368        type_id: &pax_manifest::TypeId,
1369    ) -> std::rc::Rc<dyn crate::rendering::InstanceNode> {
1370        let descriptor = self
1371            .get_component_descriptor(type_id)
1372            .expect("Failed to get component descriptor");
1373        let args = self.build_component_args(type_id);
1374        instantiate_component_from_descriptor(descriptor, args)
1375    }
1376
1377    fn get_component_descriptor(
1378        &self,
1379        type_id: &pax_manifest::TypeId,
1380    ) -> Option<&'static ErasedComponentDescriptor>;
1381
1382    fn build_component_args(
1383        &self,
1384        type_id: &pax_manifest::TypeId,
1385    ) -> crate::rendering::InstantiationArgs {
1386        let manifest: std::cell::Ref<pax_manifest::PaxManifest> = self.get_manifest();
1387        if let None = manifest.components.get(type_id) {
1388            panic!("Components with type_id {} not found in manifest", type_id);
1389        }
1390        let component = manifest.components.get(type_id).unwrap();
1391        let descriptor = self
1392            .get_component_descriptor(&type_id)
1393            .expect(&format!("No component descriptor for type: {}", type_id));
1394        let prototypical_common_properties = crate::CommonPropertiesInit::Default;
1395        let mut prototypical_properties = crate::PropertiesInit::DescriptorDefault(descriptor);
1396
1397        // pull handlers for this component
1398        let handlers = manifest.get_component_handlers(type_id);
1399        let handler_registry = Some(build_component_handler_registry_for_descriptor(
1400            descriptor, handlers,
1401        ));
1402
1403        let mut component_template = None;
1404        if let Some(template) = &component.template {
1405            let root = template.get_root();
1406            let mut instances = Vec::new();
1407            for node_id in root {
1408                let node = template.get_node(&node_id).unwrap();
1409                match node.type_id.get_pax_type() {
1410                    pax_manifest::PaxType::If
1411                    | pax_manifest::PaxType::Router
1412                    | pax_manifest::PaxType::Slot
1413                    | pax_manifest::PaxType::Repeat => {
1414                        instances.push(self.build_control_flow(type_id, &node_id, None));
1415                    }
1416                    pax_manifest::PaxType::Comment => continue,
1417                    _ => {
1418                        instances.push(self.build_template_node(type_id, &node_id, None));
1419                    }
1420                }
1421            }
1422            component_template = Some(RefCell::new(instances));
1423        }
1424
1425        let mut component_self_timeline_properties = BTreeMap::new();
1426        manifest.merge_component_self_timelines_with_properties(
1427            type_id,
1428            &mut component_self_timeline_properties,
1429        );
1430        if !component_self_timeline_properties.is_empty() {
1431            prototypical_properties = crate::PropertiesInit::DescriptorInline {
1432                descriptor,
1433                defined_properties: component_self_timeline_properties,
1434            };
1435        }
1436
1437        crate::rendering::InstantiationArgs {
1438            prototypical_common_properties,
1439            prototypical_properties,
1440            handler_registry,
1441            component_template,
1442            children: None,
1443            component_settings: component.settings.clone(),
1444            template_node_identifier: None,
1445            template_node_type_id: None,
1446            template_node_selector_info: None,
1447            transition_config: manifest.get_component_transition_config(type_id),
1448            properties_scope: crate::PropertiesScopeInit::Descriptor(descriptor),
1449        }
1450    }
1451
1452    fn build_control_flow(
1453        &self,
1454        containing_component_type_id: &pax_manifest::TypeId,
1455        node_id: &pax_manifest::TemplateNodeId,
1456        prior_node: Option<ReusableInstanceNodeArgs>,
1457    ) -> std::rc::Rc<dyn crate::rendering::InstanceNode> {
1458        let manifest = self.get_manifest();
1459        let prototypical_common_properties = crate::CommonPropertiesInit::Default;
1460
1461        let containing_component = manifest
1462            .components
1463            .get(containing_component_type_id)
1464            .unwrap();
1465        let containing_template = containing_component.template.as_ref().unwrap();
1466        let tnd = containing_template.get_node(node_id).unwrap();
1467        let unique_identifier = pax_manifest::UniqueTemplateNodeIdentifier::build(
1468            containing_component_type_id.clone(),
1469            node_id.clone(),
1470        );
1471
1472        let children: RefCell<Vec<Rc<dyn InstanceNode>>> = if let Some(prior_node) = prior_node {
1473            prior_node.children
1474        } else {
1475            RefCell::new(self.build_children(containing_component_type_id, &node_id))
1476        };
1477        match tnd.type_id.get_pax_type() {
1478            pax_manifest::PaxType::If => {
1479                let control_flow_settings = tnd.control_flow_settings.as_ref().unwrap();
1480                let conditional_branch_definitions =
1481                    if control_flow_settings.conditional_branches.is_empty() {
1482                        control_flow_settings
1483                            .condition_expression
1484                            .clone()
1485                            .map(|condition| {
1486                                vec![(
1487                                    pax_manifest::ControlFlowConditionalBranchKind::If,
1488                                    Some(condition),
1489                                )]
1490                            })
1491                            .unwrap_or_default()
1492                    } else {
1493                        control_flow_settings
1494                            .conditional_branches
1495                            .iter()
1496                            .map(|branch| {
1497                                (
1498                                    branch.branch_kind.clone(),
1499                                    branch.condition_expression.clone(),
1500                                )
1501                            })
1502                            .collect::<Vec<_>>()
1503                    };
1504                let branch_child_ranges = if control_flow_settings.conditional_branches.is_empty() {
1505                    vec![]
1506                } else {
1507                    let mut start = 0;
1508                    control_flow_settings
1509                        .conditional_branches
1510                        .iter()
1511                        .map(|branch| {
1512                            let branch_instance_count = branch
1513                                .child_ids
1514                                .iter()
1515                                .filter(|child_id| {
1516                                    containing_template
1517                                        .get_node(child_id)
1518                                        .map(|child| {
1519                                            child.type_id.get_pax_type()
1520                                                != &pax_manifest::PaxType::Comment
1521                                        })
1522                                        .unwrap_or(false)
1523                                })
1524                                .count();
1525                            let end = start + branch_instance_count;
1526                            let range = start..end;
1527                            start = end;
1528                            range
1529                        })
1530                        .collect()
1531                };
1532                let prototypical_properties_factory: Box<
1533                    dyn Fn(
1534                        std::rc::Rc<crate::RuntimePropertiesStackFrame>,
1535                        Option<std::rc::Rc<ExpandedNode>>,
1536                    )
1537                        -> Option<std::rc::Rc<RefCell<pax_runtime_api::pax_value::PaxAny>>>,
1538                > = Box::new(move |stack_frame, expanded_node| {
1539                    let conditional_branch_properties = conditional_branch_definitions
1540                        .iter()
1541                        .cloned()
1542                        .map(|(branch_kind, condition_expression)| {
1543                            build_conditional_branch_property(
1544                                branch_kind,
1545                                condition_expression,
1546                                stack_frame.clone(),
1547                            )
1548                        })
1549                        .collect::<Vec<_>>();
1550                    let boolean_expression = conditional_branch_properties
1551                        .first()
1552                        .cloned()
1553                        .unwrap_or_else(|| LocalProperty::new(false));
1554
1555                    if let Some(expanded_node) = &expanded_node {
1556                        let expanded_node = borrow!(**expanded_node);
1557                        let outer_ref = expanded_node.properties.borrow();
1558                        let rc = Rc::clone(&outer_ref);
1559                        let mut inner_ref = (*rc).borrow_mut();
1560                        let cp = ConditionalProperties::mut_from_pax_any(&mut inner_ref).unwrap();
1561                        cp.boolean_expression.replace_with(boolean_expression);
1562                        cp.conditional_branches = conditional_branch_properties;
1563                        return None;
1564                    }
1565
1566                    Some(std::rc::Rc::new(RefCell::new({
1567                        let mut properties = crate::ConditionalProperties::default();
1568                        properties.boolean_expression = boolean_expression;
1569                        properties.conditional_branches = conditional_branch_properties;
1570                        properties.to_pax_any()
1571                    })))
1572                });
1573                crate::ConditionalInstance::instantiate_with_branch_child_ranges(
1574                    crate::rendering::InstantiationArgs {
1575                        prototypical_common_properties,
1576                        prototypical_properties: crate::PropertiesInit::Factory(
1577                            prototypical_properties_factory,
1578                        ),
1579                        handler_registry: None,
1580                        component_template: None,
1581                        children: Some(children),
1582                        component_settings: None,
1583                        template_node_identifier: Some(unique_identifier),
1584                        template_node_type_id: Some(tnd.type_id.clone()),
1585                        template_node_selector_info: Some(tnd.selector_info.clone()),
1586                        transition_config: Default::default(),
1587                        properties_scope: crate::PropertiesScopeInit::None,
1588                    },
1589                    branch_child_ranges,
1590                )
1591            }
1592            pax_manifest::PaxType::Router => {
1593                let control_flow_settings = tnd.control_flow_settings.as_ref().unwrap();
1594                let route_branch_definitions = control_flow_settings.route_branches.clone();
1595                let compiled_route_branches =
1596                    crate::compile_route_branches(&route_branch_definitions);
1597                let branch_child_ranges = if route_branch_definitions.is_empty() {
1598                    vec![]
1599                } else {
1600                    let mut start = 0;
1601                    route_branch_definitions
1602                        .iter()
1603                        .map(|branch| {
1604                            let branch_instance_count = branch
1605                                .child_ids
1606                                .iter()
1607                                .filter(|child_id| {
1608                                    containing_template
1609                                        .get_node(child_id)
1610                                        .map(|child| {
1611                                            child.type_id.get_pax_type()
1612                                                != &pax_manifest::PaxType::Comment
1613                                        })
1614                                        .unwrap_or(false)
1615                                })
1616                                .count();
1617                            let end = start + branch_instance_count;
1618                            let range = start..end;
1619                            start = end;
1620                            range
1621                        })
1622                        .collect()
1623                };
1624
1625                let prototypical_properties_factory: Box<
1626                    dyn Fn(
1627                        std::rc::Rc<crate::RuntimePropertiesStackFrame>,
1628                        Option<std::rc::Rc<ExpandedNode>>,
1629                    )
1630                        -> Option<std::rc::Rc<RefCell<pax_runtime_api::pax_value::PaxAny>>>,
1631                > = Box::new(move |stack_frame, expanded_node| {
1632                    let global_location = stack_frame
1633                        .resolve_symbol_as_erased_property(INTERNAL_ROUTE_LOCATION_SYMBOL)
1634                        .map(LocalProperty::<RouteLocation>::new_from_untyped)
1635                        .unwrap_or_else(|| LocalProperty::new(RouteLocation::root()));
1636                    let input_location = stack_frame
1637                        .resolve_symbol_as_erased_property(INTERNAL_ROUTE_MATCH_SYMBOL)
1638                        .map(LocalProperty::<RouteMatch>::new_from_untyped)
1639                        .map(|route_match| {
1640                            let dependency = route_match.untyped();
1641                            LocalProperty::computed_with_name(
1642                                move || route_match.get().remainder_location(),
1643                                &[dependency],
1644                                "router input location",
1645                            )
1646                        })
1647                        .unwrap_or_else(|| global_location.clone());
1648
1649                    if let Some(expanded_node) = &expanded_node {
1650                        let expanded_node = borrow!(**expanded_node);
1651                        let outer_ref = expanded_node.properties.borrow();
1652                        let rc = Rc::clone(&outer_ref);
1653                        let mut inner_ref = (*rc).borrow_mut();
1654                        let router =
1655                            crate::RouterProperties::mut_from_pax_any(&mut inner_ref).unwrap();
1656                        router.input_location.replace_with(input_location);
1657                        router.global_location.replace_with(global_location);
1658                        return None;
1659                    }
1660
1661                    Some(std::rc::Rc::new(RefCell::new({
1662                        let mut properties = crate::RouterProperties::default();
1663                        properties.input_location = input_location;
1664                        properties.global_location = global_location;
1665                        properties.to_pax_any()
1666                    })))
1667                });
1668
1669                crate::RouterInstance::instantiate_with_branches(
1670                    crate::rendering::InstantiationArgs {
1671                        prototypical_common_properties,
1672                        prototypical_properties: crate::PropertiesInit::Factory(
1673                            prototypical_properties_factory,
1674                        ),
1675                        handler_registry: None,
1676                        component_template: None,
1677                        children: Some(children),
1678                        component_settings: None,
1679                        template_node_identifier: Some(unique_identifier),
1680                        template_node_type_id: Some(tnd.type_id.clone()),
1681                        template_node_selector_info: Some(tnd.selector_info.clone()),
1682                        transition_config: Default::default(),
1683                        properties_scope: crate::PropertiesScopeInit::None,
1684                    },
1685                    compiled_route_branches,
1686                    branch_child_ranges,
1687                )
1688            }
1689            pax_manifest::PaxType::Slot => {
1690                let expr_info = tnd
1691                    .control_flow_settings
1692                    .as_ref()
1693                    .unwrap()
1694                    .slot_index_expression
1695                    .clone();
1696
1697                let prototypical_properties_factory: Box<
1698                    dyn Fn(
1699                        std::rc::Rc<crate::RuntimePropertiesStackFrame>,
1700                        Option<std::rc::Rc<ExpandedNode>>,
1701                    )
1702                        -> Option<std::rc::Rc<RefCell<pax_runtime_api::pax_value::PaxAny>>>,
1703                > = Box::new(move |stack_frame, expanded_node| {
1704                    let Some(expr_info) = expr_info.clone() else {
1705                        if let Some(expanded_node) = &expanded_node {
1706                            let expanded_node = borrow!(**expanded_node);
1707                            let outer_ref = expanded_node.properties.borrow();
1708                            let rc = Rc::clone(&outer_ref);
1709                            let mut inner_ref = (*rc).borrow_mut();
1710                            let slot_properties =
1711                                crate::Slot::mut_from_pax_any(&mut inner_ref).unwrap();
1712                            slot_properties.is_remainder.set(true);
1713                            return None;
1714                        }
1715
1716                        return Some(std::rc::Rc::new(RefCell::new({
1717                            let mut properties = crate::Slot::default();
1718                            properties.is_remainder = LocalProperty::new(true);
1719                            properties.to_pax_any()
1720                        })));
1721                    };
1722
1723                    let cloned_stack = stack_frame.clone();
1724                    let expr_ast = expr_info.expression.clone();
1725
1726                    let mut dependencies = Vec::new();
1727                    for dependency in &expr_info.dependencies {
1728                        if let Some(p) = stack_frame.resolve_symbol_as_erased_property(dependency) {
1729                            dependencies.push(p);
1730                        } else {
1731                            log::warn!("Failed to resolve symbol {}", dependency);
1732                        }
1733                    }
1734
1735                    if let Some(expanded_node) = &expanded_node {
1736                        let expanded_node = borrow!(**expanded_node);
1737                        let outer_ref = expanded_node.properties.borrow();
1738                        let rc = Rc::clone(&outer_ref);
1739                        let mut inner_ref = (*rc).borrow_mut();
1740                        let slot_properties =
1741                            crate::Slot::mut_from_pax_any(&mut inner_ref).unwrap();
1742                        slot_properties
1743                            .index
1744                            .replace_with(LocalProperty::computed_with_name(
1745                                move || {
1746                                    let new_value = expr_ast
1747                                        .compute(cloned_stack.clone())
1748                                        .unwrap_or_else(|op_err| {
1749                                            log::warn!(
1750                                                "Failed to compute expression: {:?}",
1751                                                op_err
1752                                            );
1753                                            Default::default()
1754                                        });
1755                                    let coerced: Numeric = Numeric::try_coerce(new_value)
1756                                        .unwrap_or_else(|_| {
1757                                            log::warn!(
1758                                                "Failed to parse slot index expression: {}",
1759                                                expr_ast
1760                                            );
1761                                            Default::default()
1762                                        });
1763                                    coerced
1764                                },
1765                                &dependencies,
1766                                "slot index",
1767                            ));
1768                        return None;
1769                    }
1770
1771                    Some(std::rc::Rc::new(RefCell::new({
1772                        let mut properties = crate::Slot::default();
1773
1774                        properties.index = LocalProperty::computed_with_name(
1775                            move || {
1776                                let new_value = expr_ast
1777                                    .compute(cloned_stack.clone())
1778                                    .unwrap_or_else(|op_err| {
1779                                        log::warn!("Failed to compute expression: {:?}", op_err);
1780                                        Default::default()
1781                                    });
1782                                let coerced: Numeric = Numeric::try_coerce(new_value)
1783                                    .unwrap_or_else(|_| {
1784                                        log::warn!(
1785                                            "Failed to parse slot index expression: {}",
1786                                            expr_ast
1787                                        );
1788                                        Default::default()
1789                                    });
1790                                coerced
1791                            },
1792                            &dependencies,
1793                            "slot index",
1794                        );
1795                        properties.to_pax_any()
1796                    })))
1797                });
1798                crate::SlotInstance::instantiate(crate::rendering::InstantiationArgs {
1799                    prototypical_common_properties,
1800                    prototypical_properties: crate::PropertiesInit::Factory(
1801                        prototypical_properties_factory,
1802                    ),
1803                    handler_registry: None,
1804                    component_template: None,
1805                    children: Some(children),
1806                    component_settings: None,
1807                    template_node_identifier: Some(unique_identifier),
1808                    template_node_type_id: Some(tnd.type_id.clone()),
1809                    template_node_selector_info: Some(tnd.selector_info.clone()),
1810                    transition_config: Default::default(),
1811                    properties_scope: crate::PropertiesScopeInit::None,
1812                })
1813            }
1814            pax_manifest::PaxType::Repeat => {
1815                let source_expression_info = tnd
1816                    .control_flow_settings
1817                    .as_ref()
1818                    .unwrap()
1819                    .repeat_source_expression
1820                    .clone()
1821                    .unwrap();
1822                let repeat_key_expression = tnd
1823                    .control_flow_settings
1824                    .as_ref()
1825                    .unwrap()
1826                    .repeat_key_expression
1827                    .clone();
1828                let predictate_definition = tnd
1829                    .control_flow_settings
1830                    .as_ref()
1831                    .unwrap()
1832                    .repeat_predicate_definition
1833                    .clone()
1834                    .unwrap();
1835                let prototypical_properties_factory: Box<
1836                    dyn Fn(
1837                        std::rc::Rc<crate::RuntimePropertiesStackFrame>,
1838                        Option<std::rc::Rc<ExpandedNode>>,
1839                    )
1840                        -> Option<std::rc::Rc<RefCell<pax_runtime_api::pax_value::PaxAny>>>,
1841                > = Box::new(move |stack_frame, expanded_node| {
1842                    let cloned_stack = stack_frame.clone();
1843                    let expr = source_expression_info.expression.clone();
1844                    let deps = source_expression_info.dependencies.clone();
1845
1846                    let mut dependencies = Vec::new();
1847                    for dependency in &deps {
1848                        if let Some(p) = stack_frame.resolve_symbol_as_erased_property(dependency) {
1849                            dependencies.push(p);
1850                        } else {
1851                            log::warn!("Failed to resolve symbol {}", dependency);
1852                        }
1853                    }
1854
1855                    let (elem, index) = match &predictate_definition {
1856                        pax_manifest::ControlFlowRepeatPredicateDefinition::ElemId(id) => {
1857                            (Some(id.clone()), None)
1858                        }
1859                        pax_manifest::ControlFlowRepeatPredicateDefinition::ElemIdIndexId(
1860                            t1,
1861                            t2,
1862                        ) => (Some(t1.clone()), Some(t2.clone())),
1863                    };
1864
1865                    if let Some(expanded_node) = &expanded_node {
1866                        let expanded_node = borrow!(**expanded_node);
1867                        let outer_ref = expanded_node.properties.borrow();
1868                        let rc = Rc::clone(&outer_ref);
1869                        let mut inner_ref = (*rc).borrow_mut();
1870                        let repeat =
1871                            crate::RepeatProperties::mut_from_pax_any(&mut inner_ref).unwrap();
1872                        repeat
1873                            .source_expression
1874                            .replace_with(LocalProperty::computed_with_name(
1875                                move || {
1876                                    expr.compute(cloned_stack.clone()).unwrap_or_else(|op_err| {
1877                                        log::warn!("Failed to compute expression: {:?}", op_err);
1878                                        Default::default()
1879                                    })
1880                                },
1881                                &dependencies,
1882                                "repeat source vec",
1883                            ));
1884                        repeat
1885                            .iterator_i_symbol
1886                            .replace_with(LocalProperty::new(index));
1887                        repeat
1888                            .iterator_elem_symbol
1889                            .replace_with(LocalProperty::new(elem));
1890                        repeat.repeat_key_expression = repeat_key_expression.clone();
1891                        return None;
1892                    }
1893
1894                    Some(std::rc::Rc::new(RefCell::new({
1895                        let mut properties = crate::RepeatProperties::default();
1896
1897                        properties.source_expression = LocalProperty::computed_with_name(
1898                            move || {
1899                                expr.compute(cloned_stack.clone()).unwrap_or_else(|op_err| {
1900                                    log::warn!("Failed to compute expression: {:?}", op_err);
1901                                    Default::default()
1902                                })
1903                            },
1904                            &dependencies,
1905                            "repeat source vec",
1906                        );
1907
1908                        properties
1909                            .iterator_i_symbol
1910                            .replace_with(LocalProperty::new(index));
1911                        properties
1912                            .iterator_elem_symbol
1913                            .replace_with(LocalProperty::new(elem));
1914                        properties.repeat_key_expression = repeat_key_expression.clone();
1915                        properties.to_pax_any()
1916                    })))
1917                });
1918                crate::RepeatInstance::instantiate(crate::rendering::InstantiationArgs {
1919                    prototypical_common_properties,
1920                    prototypical_properties: crate::PropertiesInit::Factory(
1921                        prototypical_properties_factory,
1922                    ),
1923                    handler_registry: None,
1924                    component_template: None,
1925                    children: Some(children),
1926                    component_settings: None,
1927                    template_node_identifier: Some(unique_identifier),
1928                    template_node_type_id: Some(tnd.type_id.clone()),
1929                    template_node_selector_info: Some(tnd.selector_info.clone()),
1930                    transition_config: Default::default(),
1931                    properties_scope: crate::PropertiesScopeInit::None,
1932                })
1933            }
1934            _ => {
1935                unreachable!("Unexpected control flow type {}", tnd.type_id)
1936            }
1937        }
1938    }
1939
1940    fn build_children(
1941        &self,
1942        containing_component_type_id: &pax_manifest::TypeId,
1943        node_id: &pax_manifest::TemplateNodeId,
1944    ) -> Vec<std::rc::Rc<dyn crate::rendering::InstanceNode>> {
1945        let manifest = self.get_manifest();
1946        let containing_component = manifest
1947            .components
1948            .get(containing_component_type_id)
1949            .unwrap();
1950        let containing_template = containing_component.template.as_ref().unwrap();
1951        let children = containing_template.get_children(node_id);
1952
1953        let mut children_instances = Vec::new();
1954        for child_id in &children.unwrap_or_default() {
1955            let child = containing_template.get_node(&child_id).unwrap();
1956            match child.type_id.get_pax_type() {
1957                pax_manifest::PaxType::If
1958                | pax_manifest::PaxType::Router
1959                | pax_manifest::PaxType::Slot
1960                | pax_manifest::PaxType::Repeat => {
1961                    children_instances.push(self.build_control_flow(
1962                        containing_component_type_id,
1963                        &child_id,
1964                        None,
1965                    ));
1966                }
1967                pax_manifest::PaxType::Comment => continue,
1968                _ => {
1969                    children_instances.push(self.build_template_node(
1970                        containing_component_type_id,
1971                        child_id,
1972                        None,
1973                    ));
1974                }
1975            }
1976        }
1977        children_instances
1978    }
1979
1980    fn build_template_node(
1981        &self,
1982        containing_component_type_id: &pax_manifest::TypeId,
1983        node_id: &pax_manifest::TemplateNodeId,
1984        prior_node: Option<ReusableInstanceNodeArgs>,
1985    ) -> std::rc::Rc<dyn crate::rendering::InstanceNode> {
1986        let manifest = self.get_manifest();
1987
1988        let containing_component = manifest
1989            .components
1990            .get(containing_component_type_id)
1991            .unwrap();
1992        let containing_template = containing_component.template.as_ref().unwrap();
1993        let node = containing_template.get_node(node_id).unwrap();
1994        let node = node.clone();
1995        let containing_component_settings = containing_component.settings.clone();
1996        let containing_component_timelines = containing_component.timelines.clone();
1997        let containing_component_descriptor = self
1998            .get_component_descriptor(containing_component_type_id)
1999            .unwrap();
2000
2001        let mut args = self.build_component_args(&node.type_id);
2002        let node_component_descriptor = self.get_component_descriptor(&node.type_id).unwrap();
2003
2004        if let Some(prior_node) = prior_node {
2005            args.handler_registry = prior_node.handler_registry;
2006            args.children = Some(prior_node.children);
2007            args.template_node_identifier = prior_node.template_node_identifier;
2008            args.template_node_type_id = prior_node.template_node_type_id;
2009            args.template_node_selector_info = prior_node.template_node_selector_info;
2010        } else {
2011            let handlers_from_tnd = manifest.get_inline_event_handlers(&node);
2012            let updated_registry = if let Some(registry) = args.handler_registry {
2013                add_inline_handlers_from_component_descriptor(
2014                    containing_component_descriptor,
2015                    handlers_from_tnd,
2016                    registry,
2017                )
2018            } else {
2019                add_inline_handlers_from_component_descriptor(
2020                    containing_component_descriptor,
2021                    handlers_from_tnd,
2022                    std::rc::Rc::new(RefCell::new(crate::HandlerRegistry::default())),
2023                )
2024            };
2025            // update handlers from tnd
2026            args.handler_registry = Some(updated_registry);
2027
2028            // update children from tnd
2029            args.children = Some(RefCell::new(
2030                self.build_children(containing_component_type_id, node_id),
2031            ));
2032
2033            // update id
2034            args.template_node_identifier =
2035                Some(pax_manifest::UniqueTemplateNodeIdentifier::build(
2036                    containing_component_type_id.clone(),
2037                    node_id.clone(),
2038                ));
2039            args.template_node_type_id = Some(node.type_id.clone());
2040            args.template_node_selector_info = Some(node.selector_info.clone());
2041        }
2042
2043        // update properties from tnd
2044        let mut inline_properties =
2045            manifest.get_inline_properties(containing_component_type_id, node_id, &node);
2046        manifest
2047            .merge_component_self_timelines_with_properties(&node.type_id, &mut inline_properties);
2048        let plan = Rc::new(TemplatePropertyPlan::new(
2049            node,
2050            inline_properties,
2051            containing_component_settings,
2052            containing_component_timelines,
2053        ));
2054        args.prototypical_properties = crate::PropertiesInit::Template {
2055            descriptor: node_component_descriptor,
2056            plan: Rc::clone(&plan),
2057        };
2058        args.prototypical_common_properties = crate::CommonPropertiesInit::Template(plan);
2059
2060        args.transition_config.merge_from(
2061            manifest.get_template_node_transition_config(containing_component_type_id, node_id),
2062        );
2063
2064        instantiate_component_from_descriptor(node_component_descriptor, args)
2065    }
2066
2067    fn get_template_node_by_id(
2068        &self,
2069        id: &str,
2070    ) -> Option<std::rc::Rc<dyn crate::rendering::InstanceNode>> {
2071        let manifest = self.get_manifest();
2072        let main_component_type_id = manifest.main_component_type_id.clone();
2073        let main_component = manifest.components.get(&main_component_type_id).unwrap();
2074        let template = main_component.template.as_ref().unwrap();
2075        for node_id in template.get_ids() {
2076            if let Some(found) =
2077                self.recurse_get_template_node_by_id(id, &main_component_type_id, node_id)
2078            {
2079                return Some(self.build_template_node(&found.0, &found.1, None));
2080            }
2081        }
2082        None
2083    }
2084
2085    fn check_for_id_in_template_node(
2086        &self,
2087        id: &str,
2088        tnd: &pax_manifest::TemplateNodeDefinition,
2089    ) -> bool {
2090        if let Some(settings) = &tnd.settings {
2091            for setting in settings {
2092                if let pax_manifest::SettingElement::Setting(token, value) = setting {
2093                    if &token.token_value == "id" {
2094                        if let pax_manifest::ValueDefinition::Identifier(ident) = value {
2095                            if ident.name == id {
2096                                return true;
2097                            }
2098                        }
2099                    }
2100                }
2101            }
2102        }
2103        false
2104    }
2105
2106    fn recurse_get_template_node_by_id<'a>(
2107        &'a self,
2108        id: &str,
2109        containing_component_type_id: &'a pax_manifest::TypeId,
2110        node_id: &'a pax_manifest::TemplateNodeId,
2111    ) -> Option<(pax_manifest::TypeId, pax_manifest::TemplateNodeId)> {
2112        let manifest = self.get_manifest();
2113        let containing_component = manifest
2114            .components
2115            .get(containing_component_type_id)
2116            .unwrap();
2117        let containing_template = containing_component.template.as_ref().unwrap();
2118        let tnd = containing_template.get_node(node_id).unwrap();
2119
2120        if self.check_for_id_in_template_node(id, tnd) {
2121            return Some((containing_component_type_id.clone(), node_id.clone()));
2122        }
2123
2124        if let Some(component) = &manifest.components.get(&tnd.type_id) {
2125            if let Some(template) = &component.template {
2126                for node_id in template.get_ids() {
2127                    if let Some(found) =
2128                        self.recurse_get_template_node_by_id(id, &tnd.type_id, node_id)
2129                    {
2130                        return Some(found.clone());
2131                    }
2132                }
2133            }
2134        }
2135        None
2136    }
2137}
2138
2139// Shared by rich and release-baked cartridges. Keep eligibility in the runtime
2140// rather than baking a second expression form or changing value serialization.
2141fn try_typed_property_binding<T: PropertyValue + CoercionRules>(
2142    name: &str,
2143    definition: &ValueDefinition,
2144    stack: &Rc<RuntimePropertiesStackFrame>,
2145) -> Option<LocalProperty<T>> {
2146    use pax_language::interpreter::{PaxExpression, PaxPrimary};
2147    fn identifier(expression: &PaxExpression) -> Option<&str> {
2148        match expression {
2149            PaxExpression::Primary(primary) => match primary.as_ref() {
2150                PaxPrimary::Identifier(id, accessors) if accessors.is_empty() => Some(&id.name),
2151                PaxPrimary::Grouped(expression, None) => identifier(expression),
2152                _ => None,
2153            },
2154            _ => None,
2155        }
2156    }
2157    let symbol = match definition {
2158        ValueDefinition::Identifier(id) => &id.name,
2159        ValueDefinition::Expression(info) => identifier(&info.expression)?,
2160        _ => return None,
2161    };
2162    let variable = stack.resolve_symbol_as_variable(symbol)?;
2163    if let ValueDefinition::Expression(info) = definition {
2164        // Preserve unusual/manual expression dependency metadata via fallback.
2165        if info.dependencies.len() != 1
2166            || stack
2167                .resolve_symbol_as_erased_property(&info.dependencies[0])?
2168                .get_id()
2169                != variable.get_untyped_property().get_id()
2170        {
2171            return None;
2172        }
2173    }
2174    variable.try_typed_binding(name)
2175}
2176
2177fn build_common_property_value<T>(
2178    name: &str,
2179    value_definition: &ValueDefinition,
2180    stack: &Rc<RuntimePropertiesStackFrame>,
2181) -> LocalProperty<Option<T>>
2182where
2183    T: CoercionRules + PropertyValue + ToPaxValue,
2184{
2185    if let Some(property) = try_typed_property_binding(name, value_definition, stack) {
2186        return property;
2187    }
2188    let cloned_stack = stack.clone();
2189    match value_definition {
2190        pax_manifest::ValueDefinition::LiteralValue(lv) => {
2191            let val =
2192                Option::<T>::try_coerce(resolve_literal_value(lv.clone())).unwrap_or_else(|err| {
2193                    log::warn!(
2194                        "Failed to coerce new value for property {name}. Error: {:?}",
2195                        err
2196                    );
2197                    Default::default()
2198                });
2199            LocalProperty::new_with_name(val, name)
2200        }
2201        pax_manifest::ValueDefinition::Timeline(track) => {
2202            let track = timeline_track_with_base_starting_value(track);
2203            build_timeline_property(name, &track, cloned_stack.clone())
2204        }
2205        pax_manifest::ValueDefinition::Transition(transition) => {
2206            let transition = transition_with_base_starting_value(transition);
2207            build_transition_property(name, &transition, cloned_stack.clone())
2208        }
2209        pax_manifest::ValueDefinition::DoubleBinding(identifier) => {
2210            let untyped_property =
2211                if let Some(p) = stack.resolve_symbol_as_erased_property(&identifier.name) {
2212                    p
2213                } else {
2214                    log::warn!("Failed to resolve symbol {}", identifier.name);
2215                    return Default::default();
2216                };
2217            LocalProperty::new_from_untyped(untyped_property.clone())
2218        }
2219        pax_manifest::ValueDefinition::Expression(info) => {
2220            let mut dependents = vec![];
2221            for dependency in &info.dependencies {
2222                if let Some(p) = stack.resolve_symbol_as_erased_property(dependency) {
2223                    dependents.push(p);
2224                } else {
2225                    log::warn!("Failed to resolve symbol {}", dependency);
2226                }
2227            }
2228            let cloned_ast = info.expression.clone();
2229            let expression_label = cloned_ast.to_string();
2230            let property_name = name.to_string();
2231            LocalProperty::computed_with_name(
2232                move || {
2233                    let new_value =
2234                        cloned_ast
2235                            .compute(cloned_stack.clone())
2236                            .unwrap_or_else(|error| {
2237                                log::warn!(
2238                                    "Failed to compute expr: {}: {}",
2239                                    expression_label,
2240                                    error
2241                                );
2242                                Default::default()
2243                            });
2244                    Option::<T>::try_coerce(new_value).unwrap_or_else(|err| {
2245                        log::warn!(
2246                            "Failed to coerce new value for property {property_name}. Error: {:?}",
2247                            err
2248                        );
2249                        Default::default()
2250                    })
2251                },
2252                &dependents,
2253                name,
2254            )
2255        }
2256        pax_manifest::ValueDefinition::List(_) | pax_manifest::ValueDefinition::Gradient(_) => {
2257            let mut dependents = Vec::new();
2258            collect_value_definition_dependencies(value_definition, stack, &mut dependents);
2259            let value_definition = value_definition.clone();
2260            let property_name = name.to_string();
2261            LocalProperty::computed_with_name(
2262                move || {
2263                    evaluate_value_definition_to_pax_value(&value_definition, &cloned_stack)
2264                        .and_then(|new_value| {
2265                            Option::<T>::try_coerce(new_value)
2266                                .map_err(|err| {
2267                                    log::warn!(
2268                                        "Failed to coerce new value for property {property_name}. Error: {:?}",
2269                                        err
2270                                    );
2271                                    err
2272                                })
2273                                .ok()
2274                        })
2275                        .unwrap_or_default()
2276                },
2277                &dependents,
2278                name,
2279            )
2280        }
2281        pax_manifest::ValueDefinition::Identifier(ident) => {
2282            if let Some(variable) = stack.resolve_symbol_as_variable(&ident.name) {
2283                let property_name = name.to_string();
2284                let untyped = variable.get_untyped_property().clone();
2285                let cloned_variable = variable.clone();
2286                LocalProperty::computed_with_name(
2287                    move || {
2288                        let new_value = cloned_variable.get_as_pax_value();
2289                        Option::<T>::try_coerce(new_value).unwrap_or_else(|err| {
2290                            log::warn!(
2291                                "Failed to coerce new value for property {property_name}. Error: {:?}",
2292                                err
2293                            );
2294                            Default::default()
2295                        })
2296                    },
2297                    &[untyped],
2298                    &ident.name,
2299                )
2300            } else {
2301                log::warn!("Failed to resolve symbol {}", ident.name);
2302                LocalProperty::default()
2303            }
2304        }
2305        _ => unreachable!("Invalid value definition for {name}"),
2306    }
2307}
2308
2309fn axis_component_value(value: PaxValue, axis_index: usize) -> Result<PaxValue, String> {
2310    match value {
2311        PaxValue::Vec(values) => {
2312            if values.len() != 2 {
2313                return Err(format!(
2314                    "expected 2 elements for multi-axis common property, got {}",
2315                    values.len()
2316                ));
2317            }
2318            Ok(values[axis_index].clone())
2319        }
2320        PaxValue::Option(value) => match *value {
2321            Some(value) => axis_component_value(value, axis_index),
2322            None => Ok(PaxValue::Option(Box::new(None))),
2323        },
2324        value => Ok(value),
2325    }
2326}
2327
2328fn coerce_axis_component<T>(
2329    value: PaxValue,
2330    axis_index: usize,
2331    name: &str,
2332) -> Result<Option<T>, String>
2333where
2334    T: CoercionRules + PropertyValue,
2335{
2336    let axis_value = axis_component_value(value, axis_index)?;
2337    Option::<T>::try_coerce(axis_value)
2338        .map_err(|err| format!("failed to coerce axis {axis_index} for {name}: {err}"))
2339}
2340
2341fn sample_axis_timeline_track<T: CoercionRules + PropertyValue>(
2342    track: &TimelineTrackDefinition,
2343    stack: &Rc<RuntimePropertiesStackFrame>,
2344    axis_index: usize,
2345    name: &str,
2346    initial_override: Option<&T>,
2347) -> Option<T> {
2348    match resolve_timeline_sample(track, stack, |_| true)? {
2349        TimelineSample::Value { value, initial } => {
2350            if initial {
2351                if let Some(value) = initial_override {
2352                    return Some(value.clone());
2353                }
2354            }
2355            coerce_axis_component::<T>(resolve_literal_value(value), axis_index, name)
2356                .ok()
2357                .flatten()
2358        }
2359        TimelineSample::Interpolated {
2360            current,
2361            next,
2362            easing,
2363            progress,
2364            initial,
2365        } => {
2366            let current = if initial {
2367                initial_override.cloned()
2368            } else {
2369                None
2370            }
2371            .or_else(|| {
2372                coerce_axis_component::<T>(resolve_literal_value(current), axis_index, name)
2373                    .ok()
2374                    .flatten()
2375            })?;
2376            let next = coerce_axis_component::<T>(resolve_literal_value(next), axis_index, name)
2377                .ok()
2378                .flatten()?;
2379            let curve = easing_curve_from_name(easing.as_deref());
2380            Some(curve.interpolate(&current, &next, progress))
2381        }
2382    }
2383}
2384
2385fn build_axis_timeline_property<T: CoercionRules + PropertyValue>(
2386    name: &str,
2387    track: &TimelineTrackDefinition,
2388    stack: Rc<RuntimePropertiesStackFrame>,
2389    axis_index: usize,
2390) -> LocalProperty<Option<T>> {
2391    let mut dependents = Vec::new();
2392    collect_value_definition_dependencies(
2393        &ValueDefinition::Timeline(track.clone()),
2394        &stack,
2395        &mut dependents,
2396    );
2397
2398    let cloned_stack = stack.clone();
2399    let cloned_track = track.clone();
2400    let property_name = name.to_string();
2401    LocalProperty::computed_with_name(
2402        move || {
2403            sample_axis_timeline_track(
2404                &cloned_track,
2405                &cloned_stack,
2406                axis_index,
2407                &property_name,
2408                None,
2409            )
2410        },
2411        &dependents,
2412        name,
2413    )
2414}
2415
2416fn coerce_axis_transition_starting_value<T: CoercionRules + PropertyValue>(
2417    transition: &TransitionDefinition,
2418    stack: &Rc<RuntimePropertiesStackFrame>,
2419    axis_index: usize,
2420    name: &str,
2421) -> Option<T> {
2422    transition
2423        .starting_value
2424        .as_ref()
2425        .and_then(|value| evaluate_value_definition_to_pax_value(value, stack))
2426        .and_then(|value| coerce_axis_component::<T>(value, axis_index, name).ok())
2427        .flatten()
2428}
2429
2430fn sample_axis_transition_track<T: CoercionRules + PropertyValue>(
2431    transition: &TransitionDefinition,
2432    track: Option<&TimelineTrackDefinition>,
2433    stack: &Rc<RuntimePropertiesStackFrame>,
2434    axis_index: usize,
2435    name: &str,
2436    initial_override: Option<&T>,
2437) -> Option<T> {
2438    let mut track = track?.clone();
2439    if track.starting_value.is_none() {
2440        track.starting_value = transition.starting_value.clone();
2441    }
2442    align_transition_track_playhead_to_duration(&mut track, stack);
2443    sample_axis_timeline_track(&track, stack, axis_index, name, initial_override)
2444}
2445
2446fn sample_axis_transition_property<T: CoercionRules + PropertyValue>(
2447    transition: &TransitionDefinition,
2448    stack: &Rc<RuntimePropertiesStackFrame>,
2449    axis_index: usize,
2450    name: &str,
2451    initial_override: Option<&T>,
2452) -> Option<T> {
2453    let phase = stack
2454        .resolve_symbol_as_variable(TRANSITION_PHASE_SYMBOL)
2455        .and_then(|variable| Numeric::try_coerce(variable.get_as_pax_value()).ok())
2456        .map(|value| value.to_int() as u64)
2457        .unwrap_or_default();
2458
2459    match phase {
2460        TRANSITION_PHASE_ENTER => sample_axis_transition_track(
2461            transition,
2462            transition.enter.as_ref(),
2463            stack,
2464            axis_index,
2465            name,
2466            initial_override,
2467        )
2468        .or_else(|| coerce_axis_transition_starting_value(transition, stack, axis_index, name)),
2469        TRANSITION_PHASE_EXIT => sample_axis_transition_track(
2470            transition,
2471            transition.exit.as_ref(),
2472            stack,
2473            axis_index,
2474            name,
2475            initial_override,
2476        )
2477        .or_else(|| {
2478            sample_axis_transition_track(
2479                transition,
2480                transition.enter.as_ref(),
2481                stack,
2482                axis_index,
2483                name,
2484                initial_override,
2485            )
2486        })
2487        .or_else(|| coerce_axis_transition_starting_value(transition, stack, axis_index, name)),
2488        _ => coerce_axis_transition_starting_value(transition, stack, axis_index, name),
2489    }
2490}
2491
2492fn build_axis_transition_property<T: CoercionRules + PropertyValue>(
2493    name: &str,
2494    transition: &TransitionDefinition,
2495    stack: Rc<RuntimePropertiesStackFrame>,
2496    axis_index: usize,
2497) -> LocalProperty<Option<T>> {
2498    let mut dependents = Vec::new();
2499    collect_value_definition_dependencies(
2500        &ValueDefinition::Transition(transition.clone()),
2501        &stack,
2502        &mut dependents,
2503    );
2504
2505    let cloned_stack = stack.clone();
2506    let cloned_transition = transition.clone();
2507    let property_name = name.to_string();
2508    let takeover_state = RefCell::new(TransitionTakeoverState::<T>::default());
2509    LocalProperty::computed_with_name(
2510        move || {
2511            let generation = transition_generation(&cloned_stack);
2512            let mut state = borrow_mut!(takeover_state);
2513            if generation != state.generation {
2514                let should_take_over = transition_takeover(&cloned_stack)
2515                    && active_transition_track(&cloned_transition, &cloned_stack)
2516                        .map(|track| track.interruption == InOutInterruption::Takeover)
2517                        .unwrap_or(false);
2518                state.initial_override = should_take_over
2519                    .then(|| state.last_output.clone())
2520                    .flatten();
2521                state.generation = generation;
2522            }
2523            let output = sample_axis_transition_property(
2524                &cloned_transition,
2525                &cloned_stack,
2526                axis_index,
2527                &property_name,
2528                state.initial_override.as_ref(),
2529            );
2530            state.last_output = output.clone();
2531            output
2532        },
2533        &dependents,
2534        name,
2535    )
2536}
2537
2538fn build_common_property_axis_component_value<T>(
2539    name: &str,
2540    value_definition: &ValueDefinition,
2541    stack: &Rc<RuntimePropertiesStackFrame>,
2542    axis_index: usize,
2543) -> LocalProperty<Option<T>>
2544where
2545    T: CoercionRules + PropertyValue + ToPaxValue,
2546{
2547    let cloned_stack = stack.clone();
2548    match value_definition {
2549        ValueDefinition::Timeline(track) => {
2550            let track = timeline_track_with_base_starting_value(track);
2551            build_axis_timeline_property(name, &track, cloned_stack, axis_index)
2552        }
2553        ValueDefinition::Transition(transition) => {
2554            let transition = transition_with_base_starting_value(transition);
2555            build_axis_transition_property(name, &transition, cloned_stack, axis_index)
2556        }
2557        ValueDefinition::LiteralValue(_)
2558        | ValueDefinition::List(_)
2559        | ValueDefinition::Block(_)
2560        | ValueDefinition::Gradient(_)
2561        | ValueDefinition::Expression(_)
2562        | ValueDefinition::Identifier(_)
2563        | ValueDefinition::DoubleBinding(_) => {
2564            let mut dependents = Vec::new();
2565            collect_value_definition_dependencies(value_definition, stack, &mut dependents);
2566            let value_definition = value_definition.clone();
2567            let property_name = name.to_string();
2568            LocalProperty::computed_with_name(
2569                move || {
2570                    evaluate_value_definition_to_pax_value(&value_definition, &cloned_stack)
2571                        .and_then(|value| {
2572                            coerce_axis_component::<T>(value, axis_index, &property_name)
2573                                .map_err(|err| {
2574                                    log::warn!("Failed to resolve multi-axis property: {err}");
2575                                    err
2576                                })
2577                                .ok()
2578                        })
2579                        .flatten()
2580                },
2581                &dependents,
2582                name,
2583            )
2584        }
2585        _ => unreachable!("Invalid value definition for {name}"),
2586    }
2587}
2588
2589fn resolve_property<T>(
2590    name: &str,
2591    property_columns: &RuntimeResolvedPropertyColumns,
2592    stack: &Rc<RuntimePropertiesStackFrame>,
2593) -> LocalProperty<Option<T>>
2594where
2595    T: CoercionRules + PropertyValue + ToPaxValue,
2596{
2597    let Some(entries) = property_columns.get(name) else {
2598        return LocalProperty::default();
2599    };
2600    let mut layered_property = LocalProperty::default();
2601    for entry in entries {
2602        let previous_property = layered_property.clone();
2603        let source_stack = entry.source_stack.as_ref().unwrap_or(stack);
2604        let stack_with_base = stack_with_optional_base_or(
2605            source_stack,
2606            previous_property.clone(),
2607            common_property_base_fallback(name),
2608        );
2609        let candidate_property = if let Some(axis_index) = entry.axis_index {
2610            build_common_property_axis_component_value(
2611                name,
2612                &entry.value,
2613                &stack_with_base,
2614                axis_index,
2615            )
2616        } else {
2617            build_common_property_value(name, &entry.value, &stack_with_base)
2618        };
2619        layered_property = apply_runtime_settings_condition(
2620            name,
2621            entry.condition.as_ref(),
2622            source_stack,
2623            previous_property,
2624            candidate_property,
2625        );
2626    }
2627    layered_property
2628}
2629
2630fn common_property_base_fallback<T>(name: &str) -> T
2631where
2632    T: CoercionRules + PropertyValue,
2633{
2634    // Optional common properties need the semantic layout fallback, not the Rust
2635    // type default. For example, unset `y` lays out at 0px while `Size::default()`
2636    // is 100%.
2637    let value = match name {
2638        "x" | "y" | "anchor_x" | "anchor_y" | "padding_x" | "padding_y" => {
2639            Some(PaxValue::Size(Size::ZERO()))
2640        }
2641        "rotate" | "skew_x" | "skew_y" => Some(PaxValue::Rotation(Rotation::ZERO())),
2642        "opacity" => Some(PaxValue::Numeric(1.0.into())),
2643        _ => None,
2644    };
2645
2646    value
2647        .and_then(|value| T::try_coerce(value).ok())
2648        .unwrap_or_default()
2649}
2650
2651fn collect_value_definition_dependencies(
2652    value_definition: &ValueDefinition,
2653    stack: &Rc<RuntimePropertiesStackFrame>,
2654    dependents: &mut Vec<UntypedProperty>,
2655) {
2656    match value_definition {
2657        ValueDefinition::Expression(info) => {
2658            for dependency in &info.dependencies {
2659                if let Some(property) = stack.resolve_symbol_as_erased_property(dependency) {
2660                    dependents.push(property);
2661                }
2662            }
2663        }
2664        ValueDefinition::Identifier(identifier) | ValueDefinition::DoubleBinding(identifier) => {
2665            if let Some(property) = stack.resolve_symbol_as_erased_property(&identifier.name) {
2666                dependents.push(property);
2667            }
2668        }
2669        ValueDefinition::List(values) => {
2670            for value in values {
2671                collect_value_definition_dependencies(value, stack, dependents);
2672            }
2673        }
2674        ValueDefinition::Block(block) => {
2675            for element in &block.elements {
2676                if let SettingElement::Setting(_, value_definition) = element {
2677                    collect_value_definition_dependencies(value_definition, stack, dependents);
2678                }
2679            }
2680        }
2681        ValueDefinition::Gradient(gradient) => {
2682            match &gradient.shape {
2683                GradientShapeDefinition::Mesh { rows } => {
2684                    collect_value_definition_dependencies(rows, stack, dependents);
2685                }
2686                GradientShapeDefinition::Linear { start, end } => {
2687                    if let Some(start) = start {
2688                        collect_value_definition_dependencies(start, stack, dependents);
2689                    }
2690                    if let Some(end) = end {
2691                        collect_value_definition_dependencies(end, stack, dependents);
2692                    }
2693                }
2694                GradientShapeDefinition::Radial { start, end, radius } => {
2695                    collect_value_definition_dependencies(start, stack, dependents);
2696                    collect_value_definition_dependencies(end, stack, dependents);
2697                    collect_value_definition_dependencies(radius, stack, dependents);
2698                }
2699            }
2700            for element in &gradient.elements {
2701                if let GradientElement::Stop(stop) = element {
2702                    collect_value_definition_dependencies(&stop.color, stack, dependents);
2703                }
2704            }
2705        }
2706        ValueDefinition::Timeline(track) => {
2707            if let Some(playhead) = &track.playhead {
2708                collect_value_definition_dependencies(playhead, stack, dependents);
2709            } else {
2710                if let Some(property) = stack.resolve_symbol_as_erased_property("$frames") {
2711                    dependents.push(property);
2712                }
2713                if let Some(property) = stack.resolve_symbol_as_erased_property("$millis") {
2714                    dependents.push(property);
2715                }
2716            }
2717            if let Some(duration) = &track.duration {
2718                collect_value_definition_dependencies(duration, stack, dependents);
2719            }
2720            if let Some(starting_value) = &track.starting_value {
2721                collect_value_definition_dependencies(starting_value, stack, dependents);
2722            }
2723            for element in &track.elements {
2724                if let TimelineTrackElement::Keyframe(keyframe) = element {
2725                    collect_value_definition_dependencies(&keyframe.value, stack, dependents);
2726                }
2727            }
2728        }
2729        ValueDefinition::Transition(transition) => {
2730            if let Some(property) = stack.resolve_symbol_as_erased_property(TRANSITION_PHASE_SYMBOL)
2731            {
2732                dependents.push(property);
2733            }
2734            if let Some(property) =
2735                stack.resolve_symbol_as_erased_property(TRANSITION_PLAYHEAD_SYMBOL)
2736            {
2737                dependents.push(property);
2738            }
2739            if let Some(property) =
2740                stack.resolve_symbol_as_erased_property(TRANSITION_PLAYHEAD_MILLIS_SYMBOL)
2741            {
2742                dependents.push(property);
2743            }
2744            if let Some(property) =
2745                stack.resolve_symbol_as_erased_property(TRANSITION_GENERATION_SYMBOL)
2746            {
2747                dependents.push(property);
2748            }
2749            if let Some(property) =
2750                stack.resolve_symbol_as_erased_property(TRANSITION_TAKEOVER_SYMBOL)
2751            {
2752                dependents.push(property);
2753            }
2754            if let Some(starting_value) = &transition.starting_value {
2755                collect_value_definition_dependencies(starting_value, stack, dependents);
2756            }
2757            for track in [&transition.enter, &transition.exit].into_iter().flatten() {
2758                if let Some(playhead) = &track.playhead {
2759                    collect_value_definition_dependencies(playhead, stack, dependents);
2760                }
2761                if let Some(duration) = &track.duration {
2762                    collect_value_definition_dependencies(duration, stack, dependents);
2763                }
2764                if let Some(starting_value) = &track.starting_value {
2765                    collect_value_definition_dependencies(starting_value, stack, dependents);
2766                }
2767                for element in &track.elements {
2768                    if let TimelineTrackElement::Keyframe(keyframe) = element {
2769                        collect_value_definition_dependencies(&keyframe.value, stack, dependents);
2770                    }
2771                }
2772            }
2773        }
2774        ValueDefinition::LiteralValue(_)
2775        | ValueDefinition::EventBindingTarget(_)
2776        | ValueDefinition::Undefined => {}
2777    }
2778}
2779
2780fn evaluate_literal_block_to_pax_value(
2781    block: &LiteralBlockDefinition,
2782    stack: &Rc<RuntimePropertiesStackFrame>,
2783) -> Option<PaxValue> {
2784    let mut values = Vec::new();
2785    for element in &block.elements {
2786        if let SettingElement::Setting(token, value_definition) = element {
2787            let value = evaluate_value_definition_to_pax_value(value_definition, stack)?;
2788            values.push((token.token_value.clone(), value));
2789        }
2790    }
2791    let object = PaxValue::Object(values);
2792    if let Some(name) = &block.explicit_type_pascal_identifier {
2793        if name.token_value == "Fill" || name.token_value == "Stroke" {
2794            return Some(PaxValue::Enum(Box::new((
2795                name.token_value.clone(),
2796                "__layer".into(),
2797                vec![object],
2798            ))));
2799        }
2800    }
2801    Some(object)
2802}
2803
2804fn gradient_default_start() -> PaxValue {
2805    PaxValue::Vec(vec![
2806        PaxValue::Size(Size::Percent(Numeric::F64(0.0))),
2807        PaxValue::Size(Size::Percent(Numeric::F64(0.0))),
2808    ])
2809}
2810
2811fn gradient_default_end() -> PaxValue {
2812    PaxValue::Vec(vec![
2813        PaxValue::Size(Size::Percent(Numeric::F64(100.0))),
2814        PaxValue::Size(Size::Percent(Numeric::F64(0.0))),
2815    ])
2816}
2817
2818fn evaluate_gradient_to_pax_value(
2819    gradient: &GradientDefinition,
2820    stack: &Rc<RuntimePropertiesStackFrame>,
2821) -> Option<PaxValue> {
2822    if let GradientShapeDefinition::Mesh { rows } = &gradient.shape {
2823        // An invalid dynamic record must clear the field, rather than falling through
2824        // the generic property's default paint (which is an opaque solid).
2825        let mesh = evaluate_value_definition_to_pax_value(rows, stack)
2826            .and_then(|rows| pax_runtime_api::MeshGradient::try_coerce(PaxValue::Object(vec![("rows".into(), rows)])).ok())
2827            .unwrap_or_else(|| {
2828                static REPORTED: std::sync::Once = std::sync::Once::new();
2829                REPORTED.call_once(|| log::warn!("@gradient mesh rows must contain position/color anchor records; invalid value paints no field"));
2830                pax_runtime_api::MeshGradient::default()
2831            });
2832        return Some(pax_runtime_api::Paint::MeshGradient(mesh).to_pax_value());
2833    }
2834    let stops = gradient
2835        .elements
2836        .iter()
2837        .filter_map(|element| match element {
2838            GradientElement::Stop(stop) => {
2839                let color = evaluate_value_definition_to_pax_value(&stop.color, stack)?;
2840                Some(PaxValue::Enum(Box::new((
2841                    "GradientStop".to_string(),
2842                    "get".to_string(),
2843                    vec![color, PaxValue::Size(stop.position.clone())],
2844                ))))
2845            }
2846            GradientElement::Comment(_) => None,
2847        })
2848        .collect::<Vec<_>>();
2849
2850    if stops.len() < 2 {
2851        log::warn!("@gradient requires at least two stops");
2852        return None;
2853    }
2854
2855    let stops = PaxValue::Vec(stops);
2856    let (variant, args) = match &gradient.shape {
2857        GradientShapeDefinition::Mesh { .. } => unreachable!("mesh handled before stop evaluation"),
2858        GradientShapeDefinition::Linear { start, end } => {
2859            let start = start
2860                .as_ref()
2861                .and_then(|value| evaluate_value_definition_to_pax_value(value, stack))
2862                .unwrap_or_else(gradient_default_start);
2863            let end = end
2864                .as_ref()
2865                .and_then(|value| evaluate_value_definition_to_pax_value(value, stack))
2866                .unwrap_or_else(gradient_default_end);
2867            ("linearGradient", vec![start, end, stops])
2868        }
2869        GradientShapeDefinition::Radial { start, end, radius } => {
2870            let start = evaluate_value_definition_to_pax_value(start, stack)?;
2871            let end = evaluate_value_definition_to_pax_value(end, stack)?;
2872            let radius = evaluate_value_definition_to_pax_value(radius, stack)?;
2873            ("radialGradient", vec![start, end, stops, radius])
2874        }
2875    };
2876
2877    Some(PaxValue::Enum(Box::new((
2878        "Paint".to_string(),
2879        variant.to_string(),
2880        args,
2881    ))))
2882}
2883
2884fn resolve_literal_value(value: PaxValue) -> PaxValue {
2885    match value {
2886        PaxValue::Enum(contents) => {
2887            let (scope, name, args) = *contents;
2888            let resolved_args: Vec<_> = args.into_iter().map(resolve_literal_value).collect();
2889            if Functions::has_function(&scope, &name) {
2890                call_function(scope.clone(), name.clone(), resolved_args.clone()).unwrap_or_else(
2891                    |err| {
2892                        log::warn!(
2893                            "Failed to evaluate literal helper {}::{}: {}",
2894                            scope,
2895                            name,
2896                            err
2897                        );
2898                        PaxValue::Enum(Box::new((scope, name, resolved_args)))
2899                    },
2900                )
2901            } else {
2902                PaxValue::Enum(Box::new((scope, name, resolved_args)))
2903            }
2904        }
2905        PaxValue::Vec(values) => {
2906            PaxValue::Vec(values.into_iter().map(resolve_literal_value).collect())
2907        }
2908        PaxValue::Object(values) => PaxValue::Object(
2909            values
2910                .into_iter()
2911                .map(|(key, value)| (key, resolve_literal_value(value)))
2912                .collect(),
2913        ),
2914        PaxValue::Option(value) => PaxValue::Option(Box::new(value.map(resolve_literal_value))),
2915        PaxValue::Range(start, end) => PaxValue::Range(
2916            Box::new(resolve_literal_value(*start)),
2917            Box::new(resolve_literal_value(*end)),
2918        ),
2919        value => value,
2920    }
2921}
2922
2923fn evaluate_value_definition_to_pax_value(
2924    value_definition: &ValueDefinition,
2925    stack: &Rc<RuntimePropertiesStackFrame>,
2926) -> Option<PaxValue> {
2927    match value_definition {
2928        ValueDefinition::LiteralValue(value) => Some(resolve_literal_value(value.clone())),
2929        ValueDefinition::List(values) => values
2930            .iter()
2931            .map(|value| evaluate_value_definition_to_pax_value(value, stack))
2932            .collect::<Option<Vec<_>>>()
2933            .map(PaxValue::Vec),
2934        ValueDefinition::Block(block) => evaluate_literal_block_to_pax_value(block, stack),
2935        ValueDefinition::Gradient(gradient) => evaluate_gradient_to_pax_value(gradient, stack),
2936        ValueDefinition::Expression(info) => info
2937            .expression
2938            .compute(stack.clone())
2939            .map_err(|error| {
2940                log::warn!(
2941                    "Failed to compute expression {}: {}",
2942                    info.expression,
2943                    error
2944                );
2945                error
2946            })
2947            .ok(),
2948        ValueDefinition::Identifier(identifier) | ValueDefinition::DoubleBinding(identifier) => {
2949            stack
2950                .resolve_symbol_as_variable(&identifier.name)
2951                .map(|variable| variable.get_as_pax_value())
2952        }
2953        ValueDefinition::Timeline(_) | ValueDefinition::Transition(_) => None,
2954        ValueDefinition::EventBindingTarget(_) | ValueDefinition::Undefined => None,
2955    }
2956}
2957
2958fn evaluate_settings_condition_expression(
2959    condition: &ExpressionInfo,
2960    stack: &Rc<RuntimePropertiesStackFrame>,
2961) -> bool {
2962    match condition.expression.compute(stack.clone()) {
2963        Ok(value) => bool::try_coerce(value).unwrap_or_else(|err| {
2964            log::warn!(
2965                "Settings conditional `{}` did not evaluate to bool: {}",
2966                condition,
2967                err
2968            );
2969            false
2970        }),
2971        Err(err) => {
2972            log::warn!(
2973                "Failed to compute settings conditional `{}`: {}",
2974                condition,
2975                err
2976            );
2977            false
2978        }
2979    }
2980}
2981
2982pub(crate) fn settings_condition_is_active(
2983    condition: &RuntimeSettingsCondition,
2984    stack: &Rc<RuntimePropertiesStackFrame>,
2985) -> bool {
2986    condition
2987        .negative
2988        .iter()
2989        .all(|expression| !evaluate_settings_condition_expression(expression, stack))
2990        && condition
2991            .positive
2992            .iter()
2993            .all(|expression| evaluate_settings_condition_expression(expression, stack))
2994}
2995
2996pub(crate) fn collect_settings_condition_dependencies(
2997    condition: &RuntimeSettingsCondition,
2998    stack: &Rc<RuntimePropertiesStackFrame>,
2999    dependents: &mut Vec<UntypedProperty>,
3000) {
3001    for expression in condition.positive.iter().chain(condition.negative.iter()) {
3002        for dependency in &expression.dependencies {
3003            if let Some(property) = stack.resolve_symbol_as_erased_property(dependency) {
3004                dependents.push(property);
3005            }
3006        }
3007    }
3008}
3009
3010pub fn apply_runtime_settings_condition<T>(
3011    name: &str,
3012    condition: Option<&RuntimeSettingsCondition>,
3013    stack: &Rc<RuntimePropertiesStackFrame>,
3014    previous_property: LocalProperty<T>,
3015    candidate_property: LocalProperty<T>,
3016) -> LocalProperty<T>
3017where
3018    T: PropertyValue,
3019{
3020    let Some(condition) = condition.cloned() else {
3021        return candidate_property;
3022    };
3023
3024    let mut dependents = vec![previous_property.untyped(), candidate_property.untyped()];
3025    collect_settings_condition_dependencies(&condition, stack, &mut dependents);
3026    let cloned_stack = stack.clone();
3027    LocalProperty::computed_with_name(
3028        move || {
3029            if settings_condition_is_active(&condition, &cloned_stack) {
3030                candidate_property.get()
3031            } else {
3032                previous_property.get()
3033            }
3034        },
3035        &dependents,
3036        name,
3037    )
3038}
3039
3040pub(crate) fn easing_curve_from_name(name: Option<&str>) -> EasingCurve {
3041    match name {
3042        Some("Linear") | None => EasingCurve::Linear,
3043        Some("Hold") => EasingCurve::Hold,
3044        Some("InQuad") => EasingCurve::InQuad,
3045        Some("OutQuad") => EasingCurve::OutQuad,
3046        Some("InOutQuad") => EasingCurve::InOutQuad,
3047        Some("InBack") => EasingCurve::InBack,
3048        Some("OutBack") => EasingCurve::OutBack,
3049        Some("InOutBack") => EasingCurve::InOutBack,
3050        Some(other) => {
3051            log::warn!("Unknown easing curve '{}', defaulting to Linear", other);
3052            EasingCurve::Linear
3053        }
3054    }
3055}
3056
3057#[derive(Clone, Copy, Debug, PartialEq, Eq)]
3058enum TimelineClockUnit {
3059    Frames,
3060    Millis,
3061}
3062
3063#[derive(Clone, Copy, Debug)]
3064struct TimelineScale {
3065    unit: TimelineClockUnit,
3066    total: f64,
3067}
3068
3069fn duration_to_unit(duration: Duration, unit: TimelineClockUnit) -> f64 {
3070    match unit {
3071        TimelineClockUnit::Frames => duration.as_frames_f64(),
3072        TimelineClockUnit::Millis => duration.as_milliseconds_f64(),
3073    }
3074}
3075
3076fn duration_unit(duration: Duration) -> TimelineClockUnit {
3077    if duration.is_frame_based() {
3078        TimelineClockUnit::Frames
3079    } else {
3080        TimelineClockUnit::Millis
3081    }
3082}
3083
3084pub(crate) fn evaluate_timeline_duration(
3085    value_definition: &ValueDefinition,
3086    stack: &Rc<RuntimePropertiesStackFrame>,
3087) -> Option<Duration> {
3088    evaluate_value_definition_to_pax_value(value_definition, stack)
3089        .and_then(|value| Duration::try_coerce(value).ok())
3090}
3091
3092fn timeline_duration(
3093    track: &TimelineTrackDefinition,
3094    stack: &Rc<RuntimePropertiesStackFrame>,
3095) -> Option<Duration> {
3096    track
3097        .duration
3098        .as_ref()
3099        .and_then(|duration| evaluate_timeline_duration(duration, stack))
3100}
3101
3102fn timeline_scale(
3103    track: &TimelineTrackDefinition,
3104    stack: &Rc<RuntimePropertiesStackFrame>,
3105) -> TimelineScale {
3106    if let Some(duration) = timeline_duration(track, stack) {
3107        let unit = duration_unit(duration);
3108        return TimelineScale {
3109            unit,
3110            total: duration_to_unit(duration, unit).max(0.0),
3111        };
3112    }
3113
3114    let mut max_frame: f64 = 0.0;
3115    let mut max_millis: f64 = 0.0;
3116    let mut uses_percent_markers = false;
3117    let mut uses_millis_markers = false;
3118
3119    for keyframe in track.keyframes() {
3120        match keyframe.marker {
3121            TimelineMarker::Frame(frame) => max_frame = max_frame.max(frame as f64),
3122            TimelineMarker::Duration(duration) => {
3123                if duration.is_frame_based() {
3124                    max_frame = max_frame.max(duration.as_frames_f64());
3125                } else {
3126                    uses_millis_markers = true;
3127                    max_millis = max_millis.max(duration.as_milliseconds_f64());
3128                }
3129            }
3130            TimelineMarker::Percent(_) => uses_percent_markers = true,
3131        }
3132    }
3133
3134    if uses_millis_markers {
3135        let total = if uses_percent_markers {
3136            max_millis.max(100.0)
3137        } else {
3138            max_millis
3139        };
3140        TimelineScale {
3141            unit: TimelineClockUnit::Millis,
3142            total,
3143        }
3144    } else {
3145        let total = if uses_percent_markers {
3146            max_frame.max(100.0)
3147        } else {
3148            max_frame
3149        };
3150        TimelineScale {
3151            unit: TimelineClockUnit::Frames,
3152            total,
3153        }
3154    }
3155}
3156
3157fn timeline_marker_to_position(marker: &TimelineMarker, scale: TimelineScale) -> f64 {
3158    match marker {
3159        TimelineMarker::Frame(frame) => {
3160            duration_to_unit(Duration::Frames((*frame).into()), scale.unit)
3161        }
3162        TimelineMarker::Duration(duration) => duration_to_unit(*duration, scale.unit),
3163        TimelineMarker::Percent(percent) => scale.total * (*percent / 100.0),
3164    }
3165}
3166
3167fn timeline_value_passes_coercion<T: CoercionRules>(value: &PaxValue) -> bool {
3168    T::try_coerce(resolve_literal_value(value.clone())).is_ok()
3169}
3170
3171fn evaluate_valid_timeline_value(
3172    value_definition: &ValueDefinition,
3173    stack: &Rc<RuntimePropertiesStackFrame>,
3174    is_valid_value: fn(&PaxValue) -> bool,
3175) -> Option<PaxValue> {
3176    let value = evaluate_value_definition_to_pax_value(value_definition, stack)?;
3177    is_valid_value(&value).then_some(value)
3178}
3179
3180fn loop_target_value(
3181    track: &TimelineTrackDefinition,
3182    first_keyframe: &ResolvedTimelineKeyframe,
3183    stack: &Rc<RuntimePropertiesStackFrame>,
3184    is_valid_value: fn(&PaxValue) -> bool,
3185) -> PaxValue {
3186    if first_keyframe.frame == 0.0 {
3187        first_keyframe.value.clone()
3188    } else {
3189        track
3190            .starting_value
3191            .as_ref()
3192            .and_then(|value| evaluate_valid_timeline_value(value, stack, is_valid_value))
3193            .unwrap_or_else(|| first_keyframe.value.clone())
3194    }
3195}
3196
3197struct ResolvedTimelineKeyframe {
3198    frame: f64,
3199    source_order: usize,
3200    value: PaxValue,
3201    easing: Option<String>,
3202}
3203
3204enum TimelineSample {
3205    Value {
3206        value: PaxValue,
3207        initial: bool,
3208    },
3209    Interpolated {
3210        current: PaxValue,
3211        next: PaxValue,
3212        easing: Option<String>,
3213        progress: f64,
3214        initial: bool,
3215    },
3216}
3217
3218fn sample_timeline_playhead(
3219    track: &TimelineTrackDefinition,
3220    stack: &Rc<RuntimePropertiesStackFrame>,
3221    scale: TimelineScale,
3222) -> f64 {
3223    let raw_playhead = track
3224        .playhead
3225        .as_ref()
3226        .and_then(|playhead| evaluate_value_definition_to_pax_value(playhead, stack))
3227        .and_then(|value| timeline_playhead_value_to_position(value, scale.unit))
3228        .or_else(|| {
3229            let symbol = match scale.unit {
3230                TimelineClockUnit::Frames => "$frames",
3231                TimelineClockUnit::Millis => "$millis",
3232            };
3233            stack
3234                .resolve_symbol_as_variable(symbol)
3235                .and_then(|variable| {
3236                    Numeric::try_coerce(variable.get_as_pax_value())
3237                        .ok()
3238                        .map(|value| value.to_float())
3239                })
3240        })
3241        .unwrap_or_default();
3242
3243    let repeat = track.repeat.unwrap_or(true);
3244    if !repeat {
3245        return raw_playhead.clamp(0.0, scale.total.max(0.0));
3246    }
3247
3248    let cycle_len = match scale.unit {
3249        TimelineClockUnit::Frames => (scale.total + 1.0).max(1.0),
3250        TimelineClockUnit::Millis => scale.total.max(1.0),
3251    };
3252    raw_playhead.rem_euclid(cycle_len)
3253}
3254
3255fn timeline_playhead_value_to_position(value: PaxValue, unit: TimelineClockUnit) -> Option<f64> {
3256    if let PaxValue::Duration(duration) = value {
3257        return Some(duration_to_unit(duration, unit));
3258    }
3259    Numeric::try_coerce(value)
3260        .ok()
3261        .map(|value| value.to_float())
3262}
3263
3264fn resolve_timeline_sample(
3265    track: &TimelineTrackDefinition,
3266    stack: &Rc<RuntimePropertiesStackFrame>,
3267    is_valid_value: fn(&PaxValue) -> bool,
3268) -> Option<TimelineSample> {
3269    let scale = timeline_scale(track, stack);
3270    let repeat = track.repeat.unwrap_or(true);
3271    let sample_frame = sample_timeline_playhead(track, stack, scale);
3272
3273    let mut resolved_keyframes: Vec<ResolvedTimelineKeyframe> = track
3274        .keyframes()
3275        .enumerate()
3276        .filter_map(|(source_order, keyframe): (usize, &TimelineKeyframe)| {
3277            let value = evaluate_valid_timeline_value(&keyframe.value, stack, is_valid_value)?;
3278            Some(ResolvedTimelineKeyframe {
3279                frame: timeline_marker_to_position(&keyframe.marker, scale),
3280                source_order,
3281                value,
3282                easing: keyframe
3283                    .easing
3284                    .as_ref()
3285                    .map(|token| token.token_value.clone()),
3286            })
3287        })
3288        .collect();
3289
3290    resolved_keyframes.sort_unstable_by(|lhs, rhs| {
3291        lhs.frame
3292            .partial_cmp(&rhs.frame)
3293            .unwrap_or(std::cmp::Ordering::Equal)
3294            .then_with(|| lhs.source_order.cmp(&rhs.source_order))
3295    });
3296
3297    let first_keyframe = resolved_keyframes.first()?;
3298
3299    if sample_frame < first_keyframe.frame {
3300        return Some(TimelineSample::Value {
3301            value: track
3302                .starting_value
3303                .as_ref()
3304                .and_then(|value| evaluate_valid_timeline_value(value, stack, is_valid_value))
3305                .unwrap_or_else(|| first_keyframe.value.clone()),
3306            initial: true,
3307        });
3308    }
3309
3310    for (segment_index, keyframes) in resolved_keyframes.windows(2).enumerate() {
3311        let current = &keyframes[0];
3312        let next = &keyframes[1];
3313        if sample_frame <= next.frame {
3314            if sample_frame <= current.frame {
3315                return Some(TimelineSample::Value {
3316                    value: current.value.clone(),
3317                    initial: segment_index == 0,
3318                });
3319            }
3320            let span = next.frame - current.frame;
3321            if span <= f64::EPSILON {
3322                return Some(TimelineSample::Value {
3323                    value: next.value.clone(),
3324                    initial: false,
3325                });
3326            }
3327            let progress = (sample_frame - current.frame) / span;
3328            return Some(TimelineSample::Interpolated {
3329                current: current.value.clone(),
3330                next: next.value.clone(),
3331                easing: current.easing.clone(),
3332                progress,
3333                initial: segment_index == 0,
3334            });
3335        }
3336    }
3337
3338    let last_keyframe = resolved_keyframes.last()?;
3339    if sample_frame <= last_keyframe.frame || !repeat || scale.total <= last_keyframe.frame {
3340        return Some(TimelineSample::Value {
3341            value: last_keyframe.value.clone(),
3342            initial: resolved_keyframes.len() == 1 && sample_frame <= first_keyframe.frame,
3343        });
3344    }
3345
3346    let loop_target = loop_target_value(track, first_keyframe, stack, is_valid_value);
3347    let span = scale.total - last_keyframe.frame;
3348    if span <= f64::EPSILON {
3349        return Some(TimelineSample::Value {
3350            value: loop_target,
3351            initial: false,
3352        });
3353    }
3354    let progress = (sample_frame - last_keyframe.frame) / span;
3355    Some(TimelineSample::Interpolated {
3356        current: last_keyframe.value.clone(),
3357        next: loop_target,
3358        easing: last_keyframe.easing.clone(),
3359        progress,
3360        initial: false,
3361    })
3362}
3363
3364fn sample_timeline_track<T: CoercionRules + PropertyValue>(
3365    track: &TimelineTrackDefinition,
3366    stack: &Rc<RuntimePropertiesStackFrame>,
3367    initial_override: Option<&T>,
3368) -> Option<T> {
3369    match resolve_timeline_sample(track, stack, timeline_value_passes_coercion::<T>)? {
3370        TimelineSample::Value { value, initial } => {
3371            if initial {
3372                if let Some(value) = initial_override {
3373                    return Some(value.clone());
3374                }
3375            }
3376            T::try_coerce(resolve_literal_value(value)).ok()
3377        }
3378        TimelineSample::Interpolated {
3379            current,
3380            next,
3381            easing,
3382            progress,
3383            initial,
3384        } => {
3385            let current = if initial {
3386                initial_override.cloned()
3387            } else {
3388                None
3389            }
3390            .or_else(|| T::try_coerce(resolve_literal_value(current)).ok())?;
3391            let next = T::try_coerce(resolve_literal_value(next)).ok()?;
3392            let curve = easing_curve_from_name(easing.as_deref());
3393            Some(curve.interpolate(&current, &next, progress))
3394        }
3395    }
3396}
3397
3398pub fn build_timeline_property<T: CoercionRules + PropertyValue>(
3399    name: &str,
3400    track: &TimelineTrackDefinition,
3401    stack: Rc<RuntimePropertiesStackFrame>,
3402) -> LocalProperty<T> {
3403    let mut dependents = Vec::new();
3404    if let Some(playhead) = &track.playhead {
3405        collect_value_definition_dependencies(playhead, &stack, &mut dependents);
3406    } else {
3407        if let Some(property) = stack.resolve_symbol_as_erased_property("$frames") {
3408            dependents.push(property);
3409        }
3410        if let Some(property) = stack.resolve_symbol_as_erased_property("$millis") {
3411            dependents.push(property);
3412        }
3413    }
3414    if let Some(duration) = &track.duration {
3415        collect_value_definition_dependencies(duration, &stack, &mut dependents);
3416    }
3417    if let Some(starting_value) = &track.starting_value {
3418        collect_value_definition_dependencies(starting_value, &stack, &mut dependents);
3419    }
3420    for element in &track.elements {
3421        if let TimelineTrackElement::Keyframe(keyframe) = element {
3422            collect_value_definition_dependencies(&keyframe.value, &stack, &mut dependents);
3423        }
3424    }
3425
3426    let cloned_stack = stack.clone();
3427    let cloned_track = track.clone();
3428    LocalProperty::computed_with_name(
3429        move || sample_timeline_track(&cloned_track, &cloned_stack, None).unwrap_or_default(),
3430        &dependents,
3431        name,
3432    )
3433}
3434
3435fn coerce_transition_starting_value<T: CoercionRules + PropertyValue>(
3436    transition: &TransitionDefinition,
3437    stack: &Rc<RuntimePropertiesStackFrame>,
3438) -> Option<T> {
3439    transition
3440        .starting_value
3441        .as_ref()
3442        .and_then(|value| evaluate_value_definition_to_pax_value(value, stack))
3443        .and_then(|value| T::try_coerce(value).ok())
3444}
3445
3446fn sample_transition_track<T: CoercionRules + PropertyValue>(
3447    transition: &TransitionDefinition,
3448    track: Option<&TimelineTrackDefinition>,
3449    stack: &Rc<RuntimePropertiesStackFrame>,
3450    initial_override: Option<&T>,
3451) -> Option<T> {
3452    let mut track = track?.clone();
3453    if track.starting_value.is_none() {
3454        track.starting_value = transition.starting_value.clone();
3455    }
3456    align_transition_track_playhead_to_duration(&mut track, stack);
3457    sample_timeline_track(&track, stack, initial_override)
3458}
3459
3460fn align_transition_track_playhead_to_duration(
3461    track: &mut TimelineTrackDefinition,
3462    stack: &Rc<RuntimePropertiesStackFrame>,
3463) {
3464    let Some(ValueDefinition::Identifier(identifier)) = track.playhead.as_deref() else {
3465        return;
3466    };
3467    if identifier.name != TRANSITION_PLAYHEAD_SYMBOL
3468        && identifier.name != TRANSITION_PLAYHEAD_MILLIS_SYMBOL
3469    {
3470        return;
3471    }
3472
3473    let Some(duration) = timeline_duration(track, stack) else {
3474        return;
3475    };
3476    let playhead_symbol = if duration.is_frame_based() {
3477        TRANSITION_PLAYHEAD_SYMBOL
3478    } else {
3479        TRANSITION_PLAYHEAD_MILLIS_SYMBOL
3480    };
3481    if identifier.name != playhead_symbol {
3482        track.playhead = Some(Box::new(ValueDefinition::Identifier(PaxIdentifier::new(
3483            playhead_symbol,
3484        ))));
3485    }
3486}
3487
3488fn sample_transition_property<T: CoercionRules + PropertyValue>(
3489    transition: &TransitionDefinition,
3490    stack: &Rc<RuntimePropertiesStackFrame>,
3491    initial_override: Option<&T>,
3492) -> Option<T> {
3493    let phase = stack
3494        .resolve_symbol_as_variable(TRANSITION_PHASE_SYMBOL)
3495        .and_then(|variable| Numeric::try_coerce(variable.get_as_pax_value()).ok())
3496        .map(|value| value.to_int() as u64)
3497        .unwrap_or_default();
3498
3499    match phase {
3500        TRANSITION_PHASE_ENTER => sample_transition_track(
3501            transition,
3502            transition.enter.as_ref(),
3503            stack,
3504            initial_override,
3505        )
3506        .or_else(|| coerce_transition_starting_value(transition, stack)),
3507        TRANSITION_PHASE_EXIT => sample_transition_track(
3508            transition,
3509            transition.exit.as_ref(),
3510            stack,
3511            initial_override,
3512        )
3513        .or_else(|| {
3514            sample_transition_track(
3515                transition,
3516                transition.enter.as_ref(),
3517                stack,
3518                initial_override,
3519            )
3520        })
3521        .or_else(|| coerce_transition_starting_value(transition, stack)),
3522        _ => coerce_transition_starting_value(transition, stack),
3523    }
3524}
3525
3526#[derive(Default)]
3527struct TransitionTakeoverState<T> {
3528    generation: u64,
3529    last_output: Option<T>,
3530    initial_override: Option<T>,
3531}
3532
3533fn transition_generation(stack: &Rc<RuntimePropertiesStackFrame>) -> u64 {
3534    stack
3535        .resolve_symbol_as_variable(TRANSITION_GENERATION_SYMBOL)
3536        .and_then(|variable| Numeric::try_coerce(variable.get_as_pax_value()).ok())
3537        .map(|value| value.to_int() as u64)
3538        .unwrap_or_default()
3539}
3540
3541fn transition_takeover(stack: &Rc<RuntimePropertiesStackFrame>) -> bool {
3542    stack
3543        .resolve_symbol_as_variable(TRANSITION_TAKEOVER_SYMBOL)
3544        .and_then(|variable| bool::try_coerce(variable.get_as_pax_value()).ok())
3545        .unwrap_or(false)
3546}
3547
3548fn active_transition_track<'a>(
3549    transition: &'a TransitionDefinition,
3550    stack: &Rc<RuntimePropertiesStackFrame>,
3551) -> Option<&'a TimelineTrackDefinition> {
3552    let phase = stack
3553        .resolve_symbol_as_variable(TRANSITION_PHASE_SYMBOL)
3554        .and_then(|variable| Numeric::try_coerce(variable.get_as_pax_value()).ok())
3555        .map(|value| value.to_int() as u64)
3556        .unwrap_or_default();
3557    match phase {
3558        TRANSITION_PHASE_ENTER => transition.enter.as_ref(),
3559        TRANSITION_PHASE_EXIT => transition.exit.as_ref().or(transition.enter.as_ref()),
3560        _ => None,
3561    }
3562}
3563
3564pub fn build_transition_property<T: CoercionRules + PropertyValue>(
3565    name: &str,
3566    transition: &TransitionDefinition,
3567    stack: Rc<RuntimePropertiesStackFrame>,
3568) -> LocalProperty<T> {
3569    let mut dependents = Vec::new();
3570    collect_value_definition_dependencies(
3571        &ValueDefinition::Transition(transition.clone()),
3572        &stack,
3573        &mut dependents,
3574    );
3575
3576    let cloned_stack = stack.clone();
3577    let cloned_transition = transition.clone();
3578    let takeover_state = RefCell::new(TransitionTakeoverState::<T>::default());
3579    LocalProperty::computed_with_name(
3580        move || {
3581            let generation = transition_generation(&cloned_stack);
3582            let mut state = borrow_mut!(takeover_state);
3583            if generation != state.generation {
3584                let should_take_over = transition_takeover(&cloned_stack)
3585                    && active_transition_track(&cloned_transition, &cloned_stack)
3586                        .map(|track| track.interruption == InOutInterruption::Takeover)
3587                        .unwrap_or(false);
3588                state.initial_override = should_take_over
3589                    .then(|| state.last_output.clone())
3590                    .flatten();
3591                state.generation = generation;
3592            }
3593            let output = sample_transition_property(
3594                &cloned_transition,
3595                &cloned_stack,
3596                state.initial_override.as_ref(),
3597            )
3598            .unwrap_or_default();
3599            state.last_output = Some(output.clone());
3600            output
3601        },
3602        &dependents,
3603        name,
3604    )
3605}
3606
3607/// Returns a stack frame with `$base` bound to the supplied previous-layer
3608/// property value.
3609pub fn stack_with_base<T>(
3610    stack: &Rc<RuntimePropertiesStackFrame>,
3611    base_property: LocalProperty<T>,
3612) -> Rc<RuntimePropertiesStackFrame>
3613where
3614    T: PropertyValue + ToPaxValue,
3615{
3616    stack.push(HashMap::from([(
3617        BASE_SYMBOL.to_string(),
3618        Variable::new_from_typed_property(base_property),
3619    )]))
3620}
3621
3622/// Returns a stack frame with `$base` bound to an optional previous common
3623/// property value, exposing `T::default()` when the previous layer is `None`.
3624pub fn stack_with_optional_base<T>(
3625    stack: &Rc<RuntimePropertiesStackFrame>,
3626    base_property: LocalProperty<Option<T>>,
3627) -> Rc<RuntimePropertiesStackFrame>
3628where
3629    T: PropertyValue + ToPaxValue,
3630{
3631    stack_with_optional_base_or(stack, base_property, T::default())
3632}
3633
3634fn stack_with_optional_base_or<T>(
3635    stack: &Rc<RuntimePropertiesStackFrame>,
3636    base_property: LocalProperty<Option<T>>,
3637    fallback: T,
3638) -> Rc<RuntimePropertiesStackFrame>
3639where
3640    T: PropertyValue + ToPaxValue,
3641{
3642    let dependency = base_property.untyped();
3643    let cloned_base = base_property.clone();
3644    let resolved_base = LocalProperty::computed_with_name(
3645        move || cloned_base.get().unwrap_or_else(|| fallback.clone()),
3646        &[dependency],
3647        BASE_SYMBOL,
3648    );
3649    stack_with_base(stack, resolved_base)
3650}
3651
3652fn base_identifier_value() -> Box<ValueDefinition> {
3653    Box::new(ValueDefinition::Identifier(PaxIdentifier::new(BASE_SYMBOL)))
3654}
3655
3656fn timeline_track_with_base_starting_value(
3657    track: &TimelineTrackDefinition,
3658) -> TimelineTrackDefinition {
3659    let mut track = track.clone();
3660    if track.starting_value.is_none() {
3661        track.starting_value = Some(base_identifier_value());
3662    }
3663    track
3664}
3665
3666fn transition_with_base_starting_value(transition: &TransitionDefinition) -> TransitionDefinition {
3667    let mut transition = transition.clone();
3668    if transition.starting_value.is_none() {
3669        transition.starting_value = Some(base_identifier_value());
3670    }
3671    if let Some(track) = transition.enter.as_mut() {
3672        if track.starting_value.is_none() {
3673            track.starting_value = Some(base_identifier_value());
3674        }
3675    }
3676    if let Some(track) = transition.exit.as_mut() {
3677        if track.starting_value.is_none() {
3678            track.starting_value = Some(base_identifier_value());
3679        }
3680    }
3681    transition
3682}
3683
3684/// Builds a typed component property from a single value definition. Callers
3685/// that apply layered settings should bind `$base` before calling this helper.
3686pub fn build_component_property<T>(
3687    name: &str,
3688    value_definition: &ValueDefinition,
3689    stack: &Rc<RuntimePropertiesStackFrame>,
3690    timeline_stack: Rc<RuntimePropertiesStackFrame>,
3691    build_block: fn(&LiteralBlockDefinition, Rc<RuntimePropertiesStackFrame>) -> LocalProperty<T>,
3692) -> LocalProperty<T>
3693where
3694    T: CoercionRules + PropertyValue + ToPaxValue,
3695{
3696    if let Some(property) = try_typed_property_binding(name, value_definition, stack) {
3697        return property;
3698    }
3699    match value_definition {
3700        ValueDefinition::LiteralValue(lv) => {
3701            let value = T::try_coerce(resolve_literal_value(lv.clone())).unwrap_or_else(|err| {
3702                log::warn!(
3703                    "Failed to coerce new value for property {name}. Error: {:?}",
3704                    err
3705                );
3706                Default::default()
3707            });
3708            LocalProperty::new_with_name(value, name)
3709        }
3710        ValueDefinition::DoubleBinding(identifier) => {
3711            if let Some(untyped_property) =
3712                stack.resolve_symbol_as_erased_property(&identifier.name)
3713            {
3714                LocalProperty::new_from_untyped(untyped_property.clone())
3715            } else {
3716                log::warn!("Failed to resolve identifier: {}", &identifier.name);
3717                LocalProperty::new_with_name(Default::default(), name)
3718            }
3719        }
3720        ValueDefinition::Identifier(ident) => {
3721            if let Some(variable) = stack.resolve_symbol_as_variable(&ident.name) {
3722                let identifier_name = ident.name.clone();
3723                let property_name = name.to_string();
3724                let untyped = variable.get_untyped_property().clone();
3725                let cloned_variable = variable.clone();
3726                LocalProperty::computed_with_name(
3727                    move || {
3728                        let new_value = cloned_variable.get_as_pax_value();
3729                        T::try_coerce(new_value).unwrap_or_else(|err| {
3730                            log::warn!(
3731                                "Failed to coerce new value for property {property_name}. Error: {:?}",
3732                                err
3733                            );
3734                            Default::default()
3735                        })
3736                    },
3737                    &[untyped],
3738                    &identifier_name,
3739                )
3740            } else {
3741                log::warn!("Failed to resolve symbol {}", ident.name);
3742                LocalProperty::new_with_name(Default::default(), name)
3743            }
3744        }
3745        ValueDefinition::Expression(info) => {
3746            let mut dependents = vec![];
3747            for dependency in &info.dependencies {
3748                if let Some(p) = stack.resolve_symbol_as_erased_property(dependency) {
3749                    dependents.push(p);
3750                } else {
3751                    log::warn!("Failed to resolve symbol {}", dependency);
3752                }
3753            }
3754            let cloned_stack = stack.clone();
3755            let cloned_ast = info.expression.clone();
3756            let expression_label = cloned_ast.to_string();
3757            let property_name = name.to_string();
3758            LocalProperty::computed_with_name(
3759                move || {
3760                    let new_value =
3761                        cloned_ast
3762                            .compute(cloned_stack.clone())
3763                            .unwrap_or_else(|error| {
3764                                log::warn!(
3765                                    "Failed to compute expr: {}: {}",
3766                                    expression_label,
3767                                    error
3768                                );
3769                                Default::default()
3770                            });
3771                    T::try_coerce(new_value).unwrap_or_else(|err| {
3772                        log::warn!(
3773                            "Failed to coerce new value for property {property_name}. Error: {:?}",
3774                            err
3775                        );
3776                        Default::default()
3777                    })
3778                },
3779                &dependents,
3780                name,
3781            )
3782        }
3783        ValueDefinition::List(_) | ValueDefinition::Gradient(_) => {
3784            let mut dependents = Vec::new();
3785            collect_value_definition_dependencies(value_definition, stack, &mut dependents);
3786            let value_definition = value_definition.clone();
3787            let cloned_stack = stack.clone();
3788            let property_name = name.to_string();
3789            LocalProperty::computed_with_name(
3790                move || {
3791                    evaluate_value_definition_to_pax_value(&value_definition, &cloned_stack)
3792                        .and_then(|new_value| {
3793                            T::try_coerce(new_value)
3794                                .map_err(|err| {
3795                                    log::warn!(
3796                                        "Failed to coerce new value for property {property_name}. Error: {:?}",
3797                                        err
3798                                    );
3799                                    err
3800                                })
3801                                .ok()
3802                        })
3803                        .unwrap_or_default()
3804                },
3805                &dependents,
3806                name,
3807            )
3808        }
3809        ValueDefinition::Timeline(track) => {
3810            let track = timeline_track_with_base_starting_value(track);
3811            build_timeline_property(name, &track, timeline_stack)
3812        }
3813        ValueDefinition::Transition(transition) => {
3814            let transition = transition_with_base_starting_value(transition);
3815            build_transition_property(name, &transition, timeline_stack)
3816        }
3817        ValueDefinition::Block(block) if T::allows_singleton_stack() => {
3818            let mut dependencies = Vec::new();
3819            collect_value_definition_dependencies(value_definition, stack, &mut dependencies);
3820            let block = block.clone();
3821            let stack = stack.clone();
3822            let name = name.to_string();
3823            LocalProperty::computed_with_name(
3824                move || {
3825                    evaluate_literal_block_to_pax_value(&block, &stack)
3826                        .and_then(|value| {
3827                            T::try_coerce(value)
3828                                .map_err(|e| log::warn!("Layer object: {e}"))
3829                                .ok()
3830                        })
3831                        .unwrap_or_default()
3832                },
3833                &dependencies,
3834                &name,
3835            )
3836        }
3837        ValueDefinition::Block(block) => build_block(block, stack.clone()),
3838        _ => unreachable!("Invalid value definition for {name}"),
3839    }
3840}
3841
3842/// Replaces a typed component property with the value produced from a single
3843/// value definition.
3844pub fn apply_component_property<T>(
3845    property: &mut impl pax_runtime_api::PropertyBinding<T>,
3846    name: &str,
3847    value_definition: &ValueDefinition,
3848    stack: &Rc<RuntimePropertiesStackFrame>,
3849    timeline_stack: Rc<RuntimePropertiesStackFrame>,
3850    build_block: fn(&LiteralBlockDefinition, Rc<RuntimePropertiesStackFrame>) -> LocalProperty<T>,
3851) where
3852    T: CoercionRules + PropertyValue + ToPaxValue,
3853{
3854    let resolved_property =
3855        build_component_property(name, value_definition, stack, timeline_stack, build_block);
3856    if matches!(value_definition, ValueDefinition::DoubleBinding(_)) {
3857        *property = pax_runtime_api::PropertyBinding::from_local(resolved_property);
3858    } else {
3859        property.local().replace_with(resolved_property);
3860    }
3861}
3862
3863#[cfg(test)]
3864mod timeline_tests {
3865    use super::build_timeline_property;
3866    use super::build_transition_property;
3867    use super::evaluate_value_definition_to_pax_value;
3868    use super::BASE_SYMBOL;
3869    use crate::RuntimePropertiesStackFrame;
3870    use pax_language::parse_pax_expression;
3871    use pax_manifest::cartridge_generation::{
3872        TRANSITION_GENERATION_SYMBOL, TRANSITION_PHASE_ENTER, TRANSITION_PHASE_EXIT,
3873        TRANSITION_PHASE_SYMBOL, TRANSITION_PLAYHEAD_MILLIS_SYMBOL, TRANSITION_PLAYHEAD_SYMBOL,
3874        TRANSITION_TAKEOVER_SYMBOL,
3875    };
3876    use pax_manifest::{
3877        ExpressionInfo, GradientDefinition, GradientElement, GradientShapeDefinition,
3878        GradientStopDefinition, InOutInterruption, PaxIdentifier, TimelineKeyframe, TimelineMarker,
3879        TimelineTrackDefinition, TimelineTrackElement, Token, TransitionDefinition,
3880        ValueDefinition,
3881    };
3882    use pax_runtime_api::pax_value::CoercionRules;
3883    use pax_runtime_api::{
3884        Color, Duration, LocalProperty, Numeric, Paint, PaxValue, Size, Variable,
3885    };
3886    use std::collections::HashMap;
3887    use std::rc::Rc;
3888    use std::sync::Arc;
3889
3890    fn build_stack_with_clocks(
3891        elapsed_frames: &LocalProperty<u64>,
3892        elapsed_millis: &LocalProperty<u64>,
3893    ) -> Rc<RuntimePropertiesStackFrame> {
3894        let scope: HashMap<String, Variable> = vec![
3895            (
3896                "$frames".to_string(),
3897                Variable::new_from_typed_property(elapsed_frames.clone()),
3898            ),
3899            (
3900                "$millis".to_string(),
3901                Variable::new_from_typed_property(elapsed_millis.clone()),
3902            ),
3903        ]
3904        .into_iter()
3905        .collect();
3906        RuntimePropertiesStackFrame::new(scope)
3907    }
3908
3909    fn build_stack(elapsed_frames: &LocalProperty<u64>) -> Rc<RuntimePropertiesStackFrame> {
3910        let elapsed_millis = LocalProperty::new(0_u64);
3911        build_stack_with_clocks(elapsed_frames, &elapsed_millis)
3912    }
3913
3914    fn expression(raw: &str) -> ValueDefinition {
3915        pax_runtime_api::pax_value::functions::Functions::register_all_functions();
3916        ValueDefinition::Expression(ExpressionInfo::new(parse_pax_expression(raw).unwrap()))
3917    }
3918
3919    #[test]
3920    fn literal_enum_helpers_evaluate_before_coercion() {
3921        pax_runtime_api::pax_value::functions::register_function(
3922            "LiteralHelper".to_string(),
3923            "make_object".to_string(),
3924            Arc::new(|mut args| {
3925                Ok(PaxValue::Object(vec![(
3926                    "value".to_string(),
3927                    args.remove(0),
3928                )]))
3929            }),
3930        );
3931        let elapsed_frames = LocalProperty::new(0_u64);
3932        let stack = build_stack(&elapsed_frames);
3933        let literal = ValueDefinition::LiteralValue(PaxValue::Enum(Box::new((
3934            "LiteralHelper".to_string(),
3935            "make_object".to_string(),
3936            vec![PaxValue::Numeric(7.0.into())],
3937        ))));
3938
3939        assert_eq!(
3940            evaluate_value_definition_to_pax_value(&literal, &stack),
3941            Some(PaxValue::Object(vec![(
3942                "value".to_string(),
3943                PaxValue::Numeric(7.0.into()),
3944            )]))
3945        );
3946    }
3947
3948    #[test]
3949    fn gradient_values_evaluate_to_default_linear_fill() {
3950        let elapsed_frames = LocalProperty::new(0_u64);
3951        let stack = build_stack(&elapsed_frames);
3952        let gradient = ValueDefinition::Gradient(GradientDefinition {
3953            shape: GradientShapeDefinition::default(),
3954            elements: vec![
3955                GradientElement::Stop(GradientStopDefinition {
3956                    position: Size::Percent(Numeric::F64(0.0)),
3957                    color: ValueDefinition::LiteralValue(PaxValue::Color(Box::new(Color::RED))),
3958                }),
3959                GradientElement::Stop(GradientStopDefinition {
3960                    position: Size::Percent(Numeric::F64(100.0)),
3961                    color: ValueDefinition::LiteralValue(PaxValue::Color(Box::new(Color::BLUE))),
3962                }),
3963            ],
3964        });
3965
3966        let value = evaluate_value_definition_to_pax_value(&gradient, &stack)
3967            .expect("gradient should evaluate");
3968        let fill = Paint::try_coerce(value).expect("gradient should coerce to Paint");
3969        let Paint::LinearGradient(linear) = fill else {
3970            panic!("expected linear gradient fill");
3971        };
3972
3973        assert!((linear.start.0.expect_percent() - 0.0).abs() < 0.0001);
3974        assert!((linear.start.1.expect_percent() - 0.0).abs() < 0.0001);
3975        assert!((linear.end.0.expect_percent() - 1.0).abs() < 0.0001);
3976        assert!((linear.end.1.expect_percent() - 0.0).abs() < 0.0001);
3977        assert_eq!(linear.stops.len(), 2);
3978        assert_eq!(linear.stops[0].color, Color::RED);
3979        assert_eq!(linear.stops[1].color, Color::BLUE);
3980    }
3981
3982    #[test]
3983    fn timeline_property_loops_over_declared_frame_range() {
3984        let elapsed_frames = LocalProperty::new(0_u64);
3985        let stack = build_stack(&elapsed_frames);
3986        let track = TimelineTrackDefinition {
3987            elements: vec![
3988                TimelineTrackElement::Keyframe(TimelineKeyframe {
3989                    marker: TimelineMarker::Frame(0),
3990                    value: ValueDefinition::LiteralValue(PaxValue::Numeric(0.0.into())),
3991                    easing: Some(Token::new_without_location("Linear".to_string())),
3992                }),
3993                TimelineTrackElement::Keyframe(TimelineKeyframe {
3994                    marker: TimelineMarker::Frame(100),
3995                    value: ValueDefinition::LiteralValue(PaxValue::Numeric(100.0.into())),
3996                    easing: None,
3997                }),
3998            ],
3999            playhead: None,
4000            duration: None,
4001            repeat: Some(true),
4002            starting_value: None,
4003            interruption: Default::default(),
4004            use_local_property_scope: false,
4005        };
4006        let property = build_timeline_property::<f64>("progress", &track, stack);
4007
4008        assert_eq!(property.get(), 0.0);
4009        elapsed_frames.set(50);
4010        assert_eq!(property.get(), 50.0);
4011        elapsed_frames.set(100);
4012        assert_eq!(property.get(), 100.0);
4013        elapsed_frames.set(101);
4014        assert_eq!(property.get(), 0.0);
4015    }
4016
4017    #[test]
4018    fn timeline_property_samples_millisecond_duration_from_elapsed_millis() {
4019        let elapsed_frames = LocalProperty::new(0_u64);
4020        let elapsed_millis = LocalProperty::new(0_u64);
4021        let stack = build_stack_with_clocks(&elapsed_frames, &elapsed_millis);
4022        let track = TimelineTrackDefinition {
4023            elements: vec![
4024                TimelineTrackElement::Keyframe(TimelineKeyframe {
4025                    marker: TimelineMarker::Percent(0.0),
4026                    value: ValueDefinition::LiteralValue(PaxValue::Numeric(0.0.into())),
4027                    easing: Some(Token::new_without_location("Linear".to_string())),
4028                }),
4029                TimelineTrackElement::Keyframe(TimelineKeyframe {
4030                    marker: TimelineMarker::Percent(100.0),
4031                    value: ValueDefinition::LiteralValue(PaxValue::Numeric(100.0.into())),
4032                    easing: None,
4033                }),
4034            ],
4035            playhead: None,
4036            duration: Some(Box::new(ValueDefinition::LiteralValue(PaxValue::Duration(
4037                pax_runtime_api::Duration::Milliseconds(1000.into()),
4038            )))),
4039            repeat: Some(false),
4040            starting_value: None,
4041            interruption: Default::default(),
4042            use_local_property_scope: false,
4043        };
4044        let property = build_timeline_property::<f64>("progress", &track, stack);
4045
4046        assert_eq!(property.get(), 0.0);
4047        elapsed_frames.set(30);
4048        assert_eq!(property.get(), 0.0);
4049        elapsed_millis.set(500);
4050        assert_eq!(property.get(), 50.0);
4051        elapsed_millis.set(1000);
4052        assert_eq!(property.get(), 100.0);
4053    }
4054
4055    #[test]
4056    fn timeline_property_holds_starting_value_before_first_keyframe() {
4057        let elapsed_frames = LocalProperty::new(0_u64);
4058        let stack = build_stack(&elapsed_frames);
4059        let track = TimelineTrackDefinition {
4060            elements: vec![
4061                TimelineTrackElement::Keyframe(TimelineKeyframe {
4062                    marker: TimelineMarker::Frame(50),
4063                    value: ValueDefinition::LiteralValue(PaxValue::Numeric(10.0.into())),
4064                    easing: Some(Token::new_without_location("Linear".to_string())),
4065                }),
4066                TimelineTrackElement::Keyframe(TimelineKeyframe {
4067                    marker: TimelineMarker::Frame(100),
4068                    value: ValueDefinition::LiteralValue(PaxValue::Numeric(0.0.into())),
4069                    easing: None,
4070                }),
4071            ],
4072            playhead: None,
4073            duration: None,
4074            repeat: Some(false),
4075            starting_value: Some(Box::new(ValueDefinition::LiteralValue(PaxValue::Numeric(
4076                5.0.into(),
4077            )))),
4078            interruption: Default::default(),
4079            use_local_property_scope: false,
4080        };
4081        let property = build_timeline_property::<f64>("progress", &track, stack);
4082
4083        assert_eq!(property.get(), 5.0);
4084        elapsed_frames.set(25);
4085        assert_eq!(property.get(), 5.0);
4086        elapsed_frames.set(75);
4087        assert_eq!(property.get(), 5.0);
4088        elapsed_frames.set(100);
4089        assert_eq!(property.get(), 0.0);
4090    }
4091
4092    #[test]
4093    fn timeline_property_interpolates_rotation_tracks() {
4094        let elapsed_frames = LocalProperty::new(0_u64);
4095        let stack = build_stack(&elapsed_frames);
4096        let track = TimelineTrackDefinition {
4097            elements: vec![
4098                TimelineTrackElement::Keyframe(TimelineKeyframe {
4099                    marker: TimelineMarker::Frame(0),
4100                    value: ValueDefinition::LiteralValue(PaxValue::Rotation(
4101                        pax_runtime_api::Rotation::Degrees((-4.0).into()),
4102                    )),
4103                    easing: Some(Token::new_without_location("Linear".to_string())),
4104                }),
4105                TimelineTrackElement::Keyframe(TimelineKeyframe {
4106                    marker: TimelineMarker::Frame(100),
4107                    value: ValueDefinition::LiteralValue(PaxValue::Rotation(
4108                        pax_runtime_api::Rotation::Degrees(8.0.into()),
4109                    )),
4110                    easing: None,
4111                }),
4112            ],
4113            playhead: None,
4114            duration: None,
4115            repeat: Some(true),
4116            starting_value: None,
4117            interruption: Default::default(),
4118            use_local_property_scope: false,
4119        };
4120        let property =
4121            build_timeline_property::<pax_runtime_api::Rotation>("rotate", &track, stack);
4122
4123        assert!((property.get().get_as_degrees() - (-4.0)).abs() < 0.0001);
4124        elapsed_frames.set(50);
4125        assert!((property.get().get_as_degrees() - 2.0).abs() < 0.0001);
4126        elapsed_frames.set(100);
4127        assert!((property.get().get_as_degrees() - 8.0).abs() < 0.0001);
4128    }
4129
4130    #[test]
4131    fn transition_rotation_tracks_can_arrive_at_base() {
4132        let phase = LocalProperty::new(TRANSITION_PHASE_ENTER);
4133        let playhead = LocalProperty::new(0.0_f64);
4134        let base = LocalProperty::new(pax_runtime_api::Rotation::Degrees(1.0.into()));
4135        let scope: HashMap<String, Variable> = vec![
4136            (
4137                TRANSITION_PHASE_SYMBOL.to_string(),
4138                Variable::new_from_typed_property(phase.clone()),
4139            ),
4140            (
4141                TRANSITION_PLAYHEAD_SYMBOL.to_string(),
4142                Variable::new_from_typed_property(playhead.clone()),
4143            ),
4144            (
4145                BASE_SYMBOL.to_string(),
4146                Variable::new_from_typed_property(base.clone()),
4147            ),
4148        ]
4149        .into_iter()
4150        .collect();
4151        let stack = RuntimePropertiesStackFrame::new(scope);
4152        let playhead_binding = Some(Box::new(ValueDefinition::Identifier(PaxIdentifier::new(
4153            TRANSITION_PLAYHEAD_SYMBOL,
4154        ))));
4155        let enter = TimelineTrackDefinition {
4156            elements: vec![
4157                TimelineTrackElement::Keyframe(TimelineKeyframe {
4158                    marker: TimelineMarker::Frame(0),
4159                    value: expression("$base + 4deg"),
4160                    easing: Some(Token::new_without_location("Linear".to_string())),
4161                }),
4162                TimelineTrackElement::Keyframe(TimelineKeyframe {
4163                    marker: TimelineMarker::Frame(10),
4164                    value: expression("$base"),
4165                    easing: None,
4166                }),
4167            ],
4168            playhead: playhead_binding,
4169            duration: None,
4170            repeat: Some(false),
4171            starting_value: None,
4172            interruption: Default::default(),
4173            use_local_property_scope: false,
4174        };
4175        let transition = TransitionDefinition {
4176            enter: Some(enter),
4177            ..Default::default()
4178        };
4179        let property =
4180            build_transition_property::<pax_runtime_api::Rotation>("rotate", &transition, stack);
4181
4182        assert!((property.get().get_as_degrees() - 5.0).abs() < 0.0001);
4183        playhead.set(10.0);
4184        assert!((property.get().get_as_degrees() - 1.0).abs() < 0.0001);
4185    }
4186
4187    #[test]
4188    fn timeline_property_samples_from_bound_playhead_property() {
4189        let elapsed_frames = LocalProperty::new(0_u64);
4190        let playhead = LocalProperty::new(0.0_f64);
4191        let scope: HashMap<String, Variable> = vec![
4192            (
4193                "$frames".to_string(),
4194                Variable::new_from_typed_property(elapsed_frames.clone()),
4195            ),
4196            (
4197                "phase".to_string(),
4198                Variable::new_from_typed_property(playhead.clone()),
4199            ),
4200        ]
4201        .into_iter()
4202        .collect();
4203        let stack = RuntimePropertiesStackFrame::new(scope);
4204        let track = TimelineTrackDefinition {
4205            elements: vec![
4206                TimelineTrackElement::Keyframe(TimelineKeyframe {
4207                    marker: TimelineMarker::Frame(0),
4208                    value: ValueDefinition::LiteralValue(PaxValue::Numeric(0.0.into())),
4209                    easing: Some(Token::new_without_location("Linear".to_string())),
4210                }),
4211                TimelineTrackElement::Keyframe(TimelineKeyframe {
4212                    marker: TimelineMarker::Frame(10),
4213                    value: ValueDefinition::LiteralValue(PaxValue::Numeric(10.0.into())),
4214                    easing: None,
4215                }),
4216            ],
4217            playhead: Some(Box::new(ValueDefinition::Identifier(
4218                pax_manifest::PaxIdentifier::new("self.phase"),
4219            ))),
4220            duration: None,
4221            repeat: Some(false),
4222            starting_value: None,
4223            interruption: Default::default(),
4224            use_local_property_scope: false,
4225        };
4226        let property = build_timeline_property::<f64>("progress", &track, stack);
4227
4228        assert_eq!(property.get(), 0.0);
4229        playhead.set(2.5);
4230        assert!((property.get() - 2.5).abs() < 0.0001);
4231        playhead.set(10.0);
4232        assert!((property.get() - 10.0).abs() < 0.0001);
4233    }
4234    #[test]
4235    fn transition_property_switches_between_enter_and_exit_tracks() {
4236        let phase = LocalProperty::new(0_u64);
4237        let playhead = LocalProperty::new(0.0_f64);
4238        let scope: HashMap<String, Variable> = vec![
4239            (
4240                TRANSITION_PHASE_SYMBOL.to_string(),
4241                Variable::new_from_typed_property(phase.clone()),
4242            ),
4243            (
4244                TRANSITION_PLAYHEAD_SYMBOL.to_string(),
4245                Variable::new_from_typed_property(playhead.clone()),
4246            ),
4247        ]
4248        .into_iter()
4249        .collect();
4250        let stack = RuntimePropertiesStackFrame::new(scope);
4251        let playhead_binding = Some(Box::new(ValueDefinition::Identifier(PaxIdentifier::new(
4252            TRANSITION_PLAYHEAD_SYMBOL,
4253        ))));
4254        let enter = TimelineTrackDefinition {
4255            elements: vec![
4256                TimelineTrackElement::Keyframe(TimelineKeyframe {
4257                    marker: TimelineMarker::Frame(0),
4258                    value: ValueDefinition::LiteralValue(PaxValue::Numeric(0.0.into())),
4259                    easing: Some(Token::new_without_location("Linear".to_string())),
4260                }),
4261                TimelineTrackElement::Keyframe(TimelineKeyframe {
4262                    marker: TimelineMarker::Frame(10),
4263                    value: ValueDefinition::LiteralValue(PaxValue::Numeric(10.0.into())),
4264                    easing: None,
4265                }),
4266            ],
4267            playhead: playhead_binding.clone(),
4268            duration: None,
4269            repeat: Some(false),
4270            starting_value: None,
4271            interruption: Default::default(),
4272            use_local_property_scope: false,
4273        };
4274        let exit = TimelineTrackDefinition {
4275            elements: vec![
4276                TimelineTrackElement::Keyframe(TimelineKeyframe {
4277                    marker: TimelineMarker::Frame(0),
4278                    value: ValueDefinition::LiteralValue(PaxValue::Numeric(10.0.into())),
4279                    easing: Some(Token::new_without_location("Linear".to_string())),
4280                }),
4281                TimelineTrackElement::Keyframe(TimelineKeyframe {
4282                    marker: TimelineMarker::Frame(10),
4283                    value: ValueDefinition::LiteralValue(PaxValue::Numeric(0.0.into())),
4284                    easing: None,
4285                }),
4286            ],
4287            playhead: playhead_binding,
4288            duration: None,
4289            repeat: Some(false),
4290            starting_value: None,
4291            interruption: Default::default(),
4292            use_local_property_scope: false,
4293        };
4294        let transition = TransitionDefinition {
4295            enter: Some(enter),
4296            exit: Some(exit),
4297            starting_value: Some(Box::new(ValueDefinition::LiteralValue(PaxValue::Numeric(
4298                5.0.into(),
4299            )))),
4300        };
4301        let property = build_transition_property::<f64>("opacity", &transition, stack);
4302
4303        assert_eq!(property.get(), 5.0);
4304        phase.set(TRANSITION_PHASE_ENTER);
4305        playhead.set(5.0);
4306        assert_eq!(property.get(), 5.0);
4307        phase.set(TRANSITION_PHASE_EXIT);
4308        assert_eq!(property.get(), 5.0);
4309        playhead.set(10.0);
4310        assert_eq!(property.get(), 0.0);
4311    }
4312
4313    #[test]
4314    fn transition_property_takes_over_from_last_typed_sample_on_reversal() {
4315        let phase = LocalProperty::new(TRANSITION_PHASE_ENTER);
4316        let playhead = LocalProperty::new(0.0_f64);
4317        let generation = LocalProperty::new(1_u64);
4318        let takeover = LocalProperty::new(false);
4319        let stack = RuntimePropertiesStackFrame::new(HashMap::from([
4320            (
4321                TRANSITION_PHASE_SYMBOL.to_string(),
4322                Variable::new_from_typed_property(phase.clone()),
4323            ),
4324            (
4325                TRANSITION_PLAYHEAD_SYMBOL.to_string(),
4326                Variable::new_from_typed_property(playhead.clone()),
4327            ),
4328            (
4329                TRANSITION_GENERATION_SYMBOL.to_string(),
4330                Variable::new_from_typed_property(generation.clone()),
4331            ),
4332            (
4333                TRANSITION_TAKEOVER_SYMBOL.to_string(),
4334                Variable::new_from_typed_property(takeover.clone()),
4335            ),
4336        ]));
4337        let track = |start: f64, end: f64, interruption| TimelineTrackDefinition {
4338            elements: vec![
4339                TimelineTrackElement::Keyframe(TimelineKeyframe {
4340                    marker: TimelineMarker::Frame(0),
4341                    value: ValueDefinition::LiteralValue(PaxValue::Numeric(start.into())),
4342                    easing: Some(Token::new_without_location("Linear".to_string())),
4343                }),
4344                TimelineTrackElement::Keyframe(TimelineKeyframe {
4345                    marker: TimelineMarker::Frame(10),
4346                    value: ValueDefinition::LiteralValue(PaxValue::Numeric(end.into())),
4347                    easing: None,
4348                }),
4349            ],
4350            playhead: Some(Box::new(ValueDefinition::Identifier(PaxIdentifier::new(
4351                TRANSITION_PLAYHEAD_SYMBOL,
4352            )))),
4353            duration: None,
4354            repeat: Some(false),
4355            starting_value: None,
4356            interruption,
4357            use_local_property_scope: false,
4358        };
4359        let transition = TransitionDefinition {
4360            enter: Some(track(0.0, 10.0, InOutInterruption::Takeover)),
4361            exit: Some(track(10.0, 0.0, InOutInterruption::Takeover)),
4362            starting_value: None,
4363        };
4364        let property = build_transition_property::<f64>("x", &transition, stack);
4365
4366        assert_eq!(property.get(), 0.0);
4367        playhead.set(4.0);
4368        assert_eq!(property.get(), 4.0);
4369
4370        playhead.set(0.0);
4371        takeover.set(true);
4372        generation.set(2);
4373        phase.set(TRANSITION_PHASE_EXIT);
4374        assert_eq!(property.get(), 4.0);
4375        playhead.set(5.0);
4376        assert_eq!(property.get(), 2.0);
4377
4378        playhead.set(0.0);
4379        generation.set(3);
4380        phase.set(TRANSITION_PHASE_ENTER);
4381        assert_eq!(property.get(), 2.0);
4382        playhead.set(5.0);
4383        assert_eq!(property.get(), 6.0);
4384    }
4385
4386    #[test]
4387    fn transition_property_restart_uses_authored_destination_start() {
4388        let phase = LocalProperty::new(TRANSITION_PHASE_ENTER);
4389        let playhead = LocalProperty::new(5.0_f64);
4390        let generation = LocalProperty::new(1_u64);
4391        let takeover = LocalProperty::new(false);
4392        let stack = RuntimePropertiesStackFrame::new(HashMap::from([
4393            (
4394                TRANSITION_PHASE_SYMBOL.to_string(),
4395                Variable::new_from_typed_property(phase.clone()),
4396            ),
4397            (
4398                TRANSITION_PLAYHEAD_SYMBOL.to_string(),
4399                Variable::new_from_typed_property(playhead.clone()),
4400            ),
4401            (
4402                TRANSITION_GENERATION_SYMBOL.to_string(),
4403                Variable::new_from_typed_property(generation.clone()),
4404            ),
4405            (
4406                TRANSITION_TAKEOVER_SYMBOL.to_string(),
4407                Variable::new_from_typed_property(takeover.clone()),
4408            ),
4409        ]));
4410        let track = |start: f64, end: f64, interruption| TimelineTrackDefinition {
4411            elements: vec![
4412                TimelineTrackElement::Keyframe(TimelineKeyframe {
4413                    marker: TimelineMarker::Frame(0),
4414                    value: ValueDefinition::LiteralValue(PaxValue::Numeric(start.into())),
4415                    easing: Some(Token::new_without_location("Linear".to_string())),
4416                }),
4417                TimelineTrackElement::Keyframe(TimelineKeyframe {
4418                    marker: TimelineMarker::Frame(10),
4419                    value: ValueDefinition::LiteralValue(PaxValue::Numeric(end.into())),
4420                    easing: None,
4421                }),
4422            ],
4423            playhead: Some(Box::new(ValueDefinition::Identifier(PaxIdentifier::new(
4424                TRANSITION_PLAYHEAD_SYMBOL,
4425            )))),
4426            duration: None,
4427            repeat: Some(false),
4428            starting_value: None,
4429            interruption,
4430            use_local_property_scope: false,
4431        };
4432        let transition = TransitionDefinition {
4433            enter: Some(track(0.0, 10.0, InOutInterruption::Takeover)),
4434            exit: Some(track(10.0, 0.0, InOutInterruption::Restart)),
4435            starting_value: None,
4436        };
4437        let property = build_transition_property::<f64>("x", &transition, stack);
4438        assert_eq!(property.get(), 5.0);
4439
4440        playhead.set(0.0);
4441        takeover.set(true);
4442        generation.set(2);
4443        phase.set(TRANSITION_PHASE_EXIT);
4444        assert_eq!(property.get(), 10.0);
4445    }
4446
4447    #[test]
4448    fn transition_property_uses_millisecond_playhead_for_dynamic_millisecond_duration() {
4449        let phase = LocalProperty::new(TRANSITION_PHASE_ENTER);
4450        let frame_playhead = LocalProperty::new(0.0_f64);
4451        let millis_playhead = LocalProperty::new(0.0_f64);
4452        let duration = LocalProperty::new(Duration::Milliseconds(1000.into()));
4453        let scope: HashMap<String, Variable> = vec![
4454            (
4455                TRANSITION_PHASE_SYMBOL.to_string(),
4456                Variable::new_from_typed_property(phase),
4457            ),
4458            (
4459                TRANSITION_PLAYHEAD_SYMBOL.to_string(),
4460                Variable::new_from_typed_property(frame_playhead.clone()),
4461            ),
4462            (
4463                TRANSITION_PLAYHEAD_MILLIS_SYMBOL.to_string(),
4464                Variable::new_from_typed_property(millis_playhead.clone()),
4465            ),
4466            (
4467                "duration".to_string(),
4468                Variable::new_from_typed_property(duration),
4469            ),
4470        ]
4471        .into_iter()
4472        .collect();
4473        let stack = RuntimePropertiesStackFrame::new(scope);
4474        let enter = TimelineTrackDefinition {
4475            elements: vec![
4476                TimelineTrackElement::Keyframe(TimelineKeyframe {
4477                    marker: TimelineMarker::Percent(0.0),
4478                    value: ValueDefinition::LiteralValue(PaxValue::Numeric(0.0.into())),
4479                    easing: Some(Token::new_without_location("Linear".to_string())),
4480                }),
4481                TimelineTrackElement::Keyframe(TimelineKeyframe {
4482                    marker: TimelineMarker::Percent(100.0),
4483                    value: ValueDefinition::LiteralValue(PaxValue::Numeric(1.0.into())),
4484                    easing: None,
4485                }),
4486            ],
4487            playhead: Some(Box::new(ValueDefinition::Identifier(PaxIdentifier::new(
4488                TRANSITION_PLAYHEAD_SYMBOL,
4489            )))),
4490            duration: Some(Box::new(ValueDefinition::Identifier(PaxIdentifier::new(
4491                "self.duration",
4492            )))),
4493            repeat: Some(false),
4494            starting_value: None,
4495            interruption: Default::default(),
4496            use_local_property_scope: false,
4497        };
4498        let transition = TransitionDefinition {
4499            enter: Some(enter),
4500            exit: None,
4501            starting_value: Some(Box::new(ValueDefinition::LiteralValue(PaxValue::Numeric(
4502                1.0.into(),
4503            )))),
4504        };
4505        let property = build_transition_property::<f64>("opacity", &transition, stack);
4506
4507        frame_playhead.set(500.0);
4508        assert_eq!(property.get(), 0.0);
4509        millis_playhead.set(500.0);
4510        assert_eq!(property.get(), 0.5);
4511        millis_playhead.set(1000.0);
4512        assert_eq!(property.get(), 1.0);
4513    }
4514
4515    #[test]
4516    fn timeline_property_skips_keyframes_that_fail_typed_coercion() {
4517        let elapsed_frames = LocalProperty::new(0_u64);
4518        let stack = build_stack(&elapsed_frames);
4519        let track = TimelineTrackDefinition {
4520            elements: vec![
4521                TimelineTrackElement::Keyframe(TimelineKeyframe {
4522                    marker: TimelineMarker::Frame(0),
4523                    value: ValueDefinition::LiteralValue(PaxValue::Numeric(0.0.into())),
4524                    easing: Some(Token::new_without_location("Linear".to_string())),
4525                }),
4526                TimelineTrackElement::Keyframe(TimelineKeyframe {
4527                    marker: TimelineMarker::Frame(50),
4528                    value: ValueDefinition::LiteralValue(PaxValue::String("bad".to_string())),
4529                    easing: Some(Token::new_without_location("Linear".to_string())),
4530                }),
4531                TimelineTrackElement::Keyframe(TimelineKeyframe {
4532                    marker: TimelineMarker::Frame(100),
4533                    value: ValueDefinition::LiteralValue(PaxValue::Numeric(100.0.into())),
4534                    easing: None,
4535                }),
4536            ],
4537            playhead: None,
4538            duration: None,
4539            repeat: Some(false),
4540            starting_value: None,
4541            interruption: Default::default(),
4542            use_local_property_scope: false,
4543        };
4544        let property = build_timeline_property::<f64>("progress", &track, stack);
4545
4546        elapsed_frames.set(75);
4547        assert!((property.get() - 75.0).abs() < 0.0001);
4548    }
4549}
4550
4551#[cfg(test)]
4552mod base_symbol_tests {
4553    use super::{
4554        build_component_property, create_new_common_properties_from_columns,
4555        property_columns_from_defined_properties, stack_with_base, RuntimePropertiesStackFrame,
4556        RuntimeResolvedPropertyColumns, RuntimeResolvedPropertyEntry, RuntimeSettingsCondition,
4557        RuntimeSettingsSource,
4558    };
4559    use pax_language::parse_pax_expression;
4560    use pax_manifest::cartridge_generation::{
4561        TRANSITION_PHASE_ENTER, TRANSITION_PHASE_SYMBOL, TRANSITION_PLAYHEAD_SYMBOL,
4562    };
4563    use pax_manifest::{
4564        ExpressionInfo, PaxIdentifier, TimelineKeyframe, TimelineMarker, TimelineTrackDefinition,
4565        TimelineTrackElement, Token, TransitionDefinition, ValueDefinition,
4566    };
4567    use pax_runtime_api::{Duration, LocalProperty, Numeric, Opacity, PaxValue, Size, Variable};
4568    use std::collections::{BTreeMap, HashMap};
4569    use std::rc::Rc;
4570
4571    fn expression(raw: &str) -> ValueDefinition {
4572        pax_runtime_api::pax_value::functions::Functions::register_all_functions();
4573        ValueDefinition::Expression(ExpressionInfo::new(parse_pax_expression(raw).unwrap()))
4574    }
4575
4576    fn size_literal(px: f64) -> ValueDefinition {
4577        ValueDefinition::LiteralValue(PaxValue::Size(Size::Pixels(Numeric::F64(px))))
4578    }
4579
4580    fn percent_literal(percent: f64) -> ValueDefinition {
4581        ValueDefinition::LiteralValue(PaxValue::Size(Size::Percent(Numeric::F64(percent))))
4582    }
4583
4584    fn size_pair_literal(x: f64, y: f64) -> ValueDefinition {
4585        ValueDefinition::LiteralValue(PaxValue::Vec(vec![
4586            PaxValue::Size(Size::Percent(Numeric::F64(x))),
4587            PaxValue::Size(Size::Percent(Numeric::F64(y))),
4588        ]))
4589    }
4590
4591    fn assert_percent(size: Option<Size>, expected: f64) {
4592        let actual = size.expect("expected common property to be set");
4593        assert!((actual.expect_percent() - (expected / 100.0)).abs() < 0.0001);
4594    }
4595
4596    fn entry(value: ValueDefinition) -> RuntimeResolvedPropertyEntry {
4597        RuntimeResolvedPropertyEntry {
4598            transition: None,
4599            source: RuntimeSettingsSource::Inline,
4600            selector: None,
4601            source_location: None,
4602            source_stack: None,
4603            value,
4604            condition: None,
4605            axis_index: None,
4606        }
4607    }
4608
4609    fn conditional_entry(value: ValueDefinition, condition: &str) -> RuntimeResolvedPropertyEntry {
4610        RuntimeResolvedPropertyEntry {
4611            condition: Some(RuntimeSettingsCondition {
4612                positive: vec![ExpressionInfo::new(
4613                    parse_pax_expression(condition).unwrap(),
4614                )],
4615                negative: Vec::new(),
4616            }),
4617            ..entry(value)
4618        }
4619    }
4620
4621    fn columns_for(
4622        name: &str,
4623        entries: Vec<RuntimeResolvedPropertyEntry>,
4624    ) -> RuntimeResolvedPropertyColumns {
4625        BTreeMap::from([(name.to_string(), entries)])
4626    }
4627
4628    fn empty_stack() -> Rc<RuntimePropertiesStackFrame> {
4629        RuntimePropertiesStackFrame::new(HashMap::new())
4630    }
4631
4632    #[test]
4633    fn pax995_dynamic_index_invalidates_component_property() {
4634        let items = LocalProperty::new(vec![vec![10_i64, 20_i64], vec![30, 40]]);
4635        let indices = LocalProperty::new(vec![0_i64, 1]);
4636        let index = LocalProperty::new(0_i64);
4637        let column = LocalProperty::new(0_i64);
4638        let stack = RuntimePropertiesStackFrame::new(HashMap::from([
4639            (
4640                "items".into(),
4641                Variable::new_from_typed_property(items.clone()),
4642            ),
4643            (
4644                "indices".into(),
4645                Variable::new_from_typed_property(indices.clone()),
4646            ),
4647            (
4648                "index".into(),
4649                Variable::new_from_typed_property(index.clone()),
4650            ),
4651            (
4652                "column".into(),
4653                Variable::new_from_typed_property(column.clone()),
4654            ),
4655        ]));
4656        let selected: LocalProperty<i64> = build_component_property(
4657            "selected",
4658            &expression("items[indices[index]][column + 0]"),
4659            &stack,
4660            stack.clone(),
4661            |_, _| unreachable!(),
4662        );
4663        assert_eq!(selected.get(), 10);
4664        index.set(1);
4665        assert_eq!(selected.get(), 30);
4666        column.set(1);
4667        assert_eq!(selected.get(), 40);
4668        indices.set(vec![1, 0]);
4669        assert_eq!(selected.get(), 20);
4670        items.set(vec![vec![50, 60], vec![70, 80]]);
4671        assert_eq!(selected.get(), 60);
4672    }
4673
4674    #[test]
4675    fn recursive_appearance_list_and_typed_paxel_objects_remain_reactive() {
4676        use pax_manifest::{LiteralBlockDefinition, SettingElement};
4677        use pax_runtime_api::{Color, Fill, Paint};
4678        let accent = LocalProperty::new(Color::RED);
4679        let stack = RuntimePropertiesStackFrame::new(HashMap::from([(
4680            "accent".into(),
4681            Variable::new_from_typed_property(accent.clone()),
4682        )]));
4683        let block = ValueDefinition::Block(LiteralBlockDefinition {
4684            explicit_type_pascal_identifier: Some(Token::new_without_location("Fill".into())),
4685            elements: vec![SettingElement::Setting(
4686                Token::new_without_location("paint".into()),
4687                expression("accent"),
4688            )],
4689        });
4690        let value =
4691            ValueDefinition::List(vec![block, expression("Fill {paint: BLUE, opacity: 50%}")]);
4692        let layers: LocalProperty<Vec<Fill>> =
4693            build_component_property("fill", &value, &stack, stack.clone(), |_, _| unreachable!());
4694        assert_eq!(layers.get()[0].paint.get(), Paint::Solid(Color::RED));
4695        assert_eq!(layers.get()[1].opacity.get().to_float_0_1(), 0.5);
4696        accent.set(Color::GREEN);
4697        assert_eq!(layers.get()[0].paint.get(), Paint::Solid(Color::GREEN));
4698        let mismatch: LocalProperty<Vec<Fill>> = build_component_property(
4699            "fill",
4700            &expression("[RED, Stroke {paint: BLUE}]"),
4701            &stack,
4702            stack.clone(),
4703            |_, _| unreachable!(),
4704        );
4705        assert!(
4706            mismatch.get().is_empty(),
4707            "an invalid list must not install a valid prefix"
4708        );
4709    }
4710
4711    #[test]
4712    fn mesh_nested_bindings_and_whole_rows_invalidate_paint() {
4713        use pax_manifest::{
4714            GradientDefinition, GradientShapeDefinition, LiteralBlockDefinition, SettingElement,
4715        };
4716        use pax_runtime_api::{Color, MeshPoint, Paint};
4717        let x = LocalProperty::new(Size::Percent(35.into()));
4718        let accent = LocalProperty::new(Color::CYAN);
4719        let rows = LocalProperty::new(vec![
4720            vec![
4721                MeshPoint {
4722                    position: (Size::default(), Size::default()),
4723                    color: Color::RED
4724                };
4725                2
4726            ];
4727            2
4728        ]);
4729        let stack = RuntimePropertiesStackFrame::new(HashMap::from([
4730            ("x".into(), Variable::new_from_typed_property(x.clone())),
4731            (
4732                "accent".into(),
4733                Variable::new_from_typed_property(accent.clone()),
4734            ),
4735            (
4736                "rows".into(),
4737                Variable::new_from_typed_property(rows.clone()),
4738            ),
4739        ]));
4740        let anchor = ValueDefinition::Block(LiteralBlockDefinition {
4741            explicit_type_pascal_identifier: None,
4742            elements: vec![
4743                SettingElement::Setting(
4744                    Token::new_without_location("position".into()),
4745                    ValueDefinition::List(vec![expression("x"), percent_literal(100.0)]),
4746                ),
4747                SettingElement::Setting(
4748                    Token::new_without_location("color".into()),
4749                    expression("accent"),
4750                ),
4751            ],
4752        });
4753        let mesh = |rows| {
4754            ValueDefinition::Gradient(GradientDefinition {
4755                shape: GradientShapeDefinition::Mesh {
4756                    rows: Box::new(rows),
4757                },
4758                elements: vec![],
4759            })
4760        };
4761        let nested: LocalProperty<Paint> = build_component_property(
4762            "paint",
4763            &mesh(ValueDefinition::List(vec![
4764                ValueDefinition::List(vec![
4765                    anchor
4766                        .clone(
4767                        );
4768                    2
4769                ]);
4770                2
4771            ])),
4772            &stack,
4773            stack.clone(),
4774            |_, _| unreachable!(),
4775        );
4776        let bound: LocalProperty<Paint> = build_component_property(
4777            "paint",
4778            &mesh(expression("rows")),
4779            &stack,
4780            stack.clone(),
4781            |_, _| unreachable!(),
4782        );
4783        let point = |paint: Paint| {
4784            let Paint::MeshGradient(mesh) = paint else {
4785                panic!("expected mesh")
4786            };
4787            mesh.rows[0][0].clone()
4788        };
4789        assert_eq!(point(nested.get()).position.0, x.get());
4790        x.set(Size::Combined(10.into(), 60.into()));
4791        accent.set(Color::YELLOW);
4792        assert_eq!(point(nested.get()).position.0, x.get());
4793        assert_eq!(point(nested.get()).color, Color::YELLOW);
4794        assert_eq!(point(bound.get()).color, Color::RED);
4795        rows.set(vec![vec![point(nested.get()); 3]; 2]);
4796        assert_eq!(point(bound.get()), point(nested.get()));
4797        let Paint::MeshGradient(mesh) = bound.get() else {
4798            unreachable!()
4799        };
4800        assert_eq!(mesh.rows[0].len(), 3);
4801        rows.set(vec![]);
4802        let malformed: LocalProperty<Paint> = build_component_property(
4803            "paint",
4804            &ValueDefinition::Gradient(GradientDefinition {
4805                shape: GradientShapeDefinition::Mesh {
4806                    rows: Box::new(expression("42")),
4807                },
4808                elements: vec![],
4809            }),
4810            &stack,
4811            stack.clone(),
4812            |_, _| unreachable!(),
4813        );
4814        assert!(matches!(malformed.get(), Paint::MeshGradient(ref value) if value.rows.is_empty()));
4815        let Paint::MeshGradient(mesh) = bound.get() else {
4816            unreachable!()
4817        };
4818        assert!(
4819            mesh.validate().is_err(),
4820            "invalid updates must replace stale valid paint"
4821        );
4822    }
4823
4824    #[test]
4825    fn base_symbol_resolves_previous_component_property_layer() {
4826        let base_property = LocalProperty::new(10.0_f64);
4827        let stack = stack_with_base(&empty_stack(), base_property);
4828
4829        let property: LocalProperty<f64> = build_component_property(
4830            "opacity",
4831            &expression("$base + 0.25"),
4832            &stack,
4833            stack.clone(),
4834            |_, _| unreachable!("literal blocks are not used by this test"),
4835        );
4836
4837        assert!((property.get() - 10.25).abs() < 0.0001);
4838    }
4839
4840    #[test]
4841    fn base_symbol_resolves_previous_common_property_layer() {
4842        let stack = empty_stack();
4843        let columns = columns_for(
4844            "x",
4845            vec![entry(size_literal(10.0)), entry(expression("$base + 5px"))],
4846        );
4847
4848        let common = create_new_common_properties_from_columns(&columns, &stack);
4849        let x = common.borrow().x.get().unwrap();
4850
4851        assert!((x.get_pixels(100.0) - 15.0).abs() < 0.0001);
4852    }
4853
4854    #[test]
4855    fn base_symbol_tracks_reactive_previous_layers() {
4856        let offset = LocalProperty::new(Size::Pixels(Numeric::F64(10.0)));
4857        let stack = RuntimePropertiesStackFrame::new(HashMap::from([(
4858            "offset".to_string(),
4859            Variable::new_from_typed_property(offset.clone()),
4860        )]));
4861        let columns = columns_for(
4862            "x",
4863            vec![
4864                entry(expression("offset")),
4865                entry(expression("$base + 5px")),
4866            ],
4867        );
4868
4869        let common = create_new_common_properties_from_columns(&columns, &stack);
4870        assert!((common.borrow().x.get().unwrap().get_pixels(100.0) - 15.0).abs() < 0.0001);
4871
4872        offset.set(Size::Pixels(Numeric::F64(20.0)));
4873        assert!((common.borrow().x.get().unwrap().get_pixels(100.0) - 25.0).abs() < 0.0001);
4874    }
4875
4876    #[test]
4877    fn imported_settings_use_the_provider_scope_reactively() {
4878        let is_dark = LocalProperty::new(false);
4879        let provider_stack = RuntimePropertiesStackFrame::new(HashMap::from([(
4880            "is_dark".to_string(),
4881            Variable::new_from_typed_property(is_dark.clone()),
4882        )]));
4883        let imported_entry = RuntimeResolvedPropertyEntry {
4884            transition: None,
4885            source: RuntimeSettingsSource::ImportedLayer {
4886                provider_id: crate::ExpandedNodeIdentifier(1),
4887                provider_type_id: pax_manifest::TypeId::build_singleton(
4888                    "example::Theme",
4889                    Some("Theme"),
4890                ),
4891            },
4892            source_stack: Some(provider_stack),
4893            ..entry(expression("self.is_dark ? 50px : 10px"))
4894        };
4895        let columns = columns_for("width", vec![imported_entry]);
4896
4897        let common = create_new_common_properties_from_columns(&columns, &empty_stack());
4898        assert!((common.borrow().width.get().unwrap().get_pixels(100.0) - 10.0).abs() < 0.0001);
4899
4900        is_dark.set(true);
4901        assert!((common.borrow().width.get().unwrap().get_pixels(100.0) - 50.0).abs() < 0.0001);
4902    }
4903
4904    #[test]
4905    fn imported_settings_conditions_use_the_provider_scope_reactively() {
4906        let is_dark = LocalProperty::new(false);
4907        let provider_stack = RuntimePropertiesStackFrame::new(HashMap::from([(
4908            "is_dark".to_string(),
4909            Variable::new_from_typed_property(is_dark.clone()),
4910        )]));
4911        let imported_entry = RuntimeResolvedPropertyEntry {
4912            transition: None,
4913            source: RuntimeSettingsSource::ImportedLayer {
4914                provider_id: crate::ExpandedNodeIdentifier(1),
4915                provider_type_id: pax_manifest::TypeId::build_singleton(
4916                    "example::Theme",
4917                    Some("Theme"),
4918                ),
4919            },
4920            source_stack: Some(provider_stack),
4921            condition: Some(RuntimeSettingsCondition {
4922                positive: vec![ExpressionInfo::new(
4923                    parse_pax_expression("self.is_dark").unwrap(),
4924                )],
4925                negative: Vec::new(),
4926            }),
4927            ..entry(size_literal(50.0))
4928        };
4929        let columns = columns_for("width", vec![entry(size_literal(10.0)), imported_entry]);
4930
4931        let common = create_new_common_properties_from_columns(&columns, &empty_stack());
4932        assert!((common.borrow().width.get().unwrap().get_pixels(100.0) - 10.0).abs() < 0.0001);
4933
4934        is_dark.set(true);
4935        assert!((common.borrow().width.get().unwrap().get_pixels(100.0) - 50.0).abs() < 0.0001);
4936    }
4937
4938    #[test]
4939    fn settings_condition_preserves_previous_layer_until_active() {
4940        let landscape = LocalProperty::new(false);
4941        let stack = RuntimePropertiesStackFrame::new(HashMap::from([(
4942            "$landscape".to_string(),
4943            Variable::new_from_typed_property(landscape.clone()),
4944        )]));
4945        let columns = columns_for(
4946            "width",
4947            vec![
4948                entry(size_literal(100.0)),
4949                conditional_entry(size_literal(50.0), "$landscape"),
4950            ],
4951        );
4952
4953        let common = create_new_common_properties_from_columns(&columns, &stack);
4954        assert!((common.borrow().width.get().unwrap().get_pixels(100.0) - 100.0).abs() < 0.0001);
4955
4956        landscape.set(true);
4957        assert!((common.borrow().width.get().unwrap().get_pixels(100.0) - 50.0).abs() < 0.0001);
4958    }
4959
4960    #[test]
4961    fn base_symbol_supplies_timeline_starting_values() {
4962        let elapsed_frames = LocalProperty::new(0_u64);
4963        let stack = RuntimePropertiesStackFrame::new(HashMap::from([(
4964            "$frames".to_string(),
4965            Variable::new_from_typed_property(elapsed_frames.clone()),
4966        )]));
4967        let track = TimelineTrackDefinition {
4968            elements: vec![
4969                TimelineTrackElement::Keyframe(TimelineKeyframe {
4970                    marker: TimelineMarker::Frame(0),
4971                    value: expression("$base - 10px"),
4972                    easing: Some(Token::new_without_location("Linear".to_string())),
4973                }),
4974                TimelineTrackElement::Keyframe(TimelineKeyframe {
4975                    marker: TimelineMarker::Frame(10),
4976                    value: expression("$base"),
4977                    easing: None,
4978                }),
4979            ],
4980            playhead: None,
4981            duration: Some(Box::new(ValueDefinition::LiteralValue(PaxValue::Duration(
4982                Duration::Frames(10.into()),
4983            )))),
4984            repeat: Some(false),
4985            starting_value: None,
4986            interruption: Default::default(),
4987            use_local_property_scope: false,
4988        };
4989        let columns = columns_for(
4990            "y",
4991            vec![
4992                entry(size_literal(100.0)),
4993                entry(ValueDefinition::Timeline(track)),
4994            ],
4995        );
4996
4997        let common = create_new_common_properties_from_columns(&columns, &stack);
4998        assert!((common.borrow().y.get().unwrap().get_pixels(100.0) - 90.0).abs() < 0.0001);
4999
5000        elapsed_frames.set(10);
5001        assert!((common.borrow().y.get().unwrap().get_pixels(100.0) - 100.0).abs() < 0.0001);
5002    }
5003
5004    #[test]
5005    fn base_symbol_uses_layout_zero_for_unset_position_transitions() {
5006        let phase = LocalProperty::new(TRANSITION_PHASE_ENTER);
5007        let playhead = LocalProperty::new(5.0_f64);
5008        let stack = RuntimePropertiesStackFrame::new(HashMap::from([
5009            (
5010                TRANSITION_PHASE_SYMBOL.to_string(),
5011                Variable::new_from_typed_property(phase.clone()),
5012            ),
5013            (
5014                TRANSITION_PLAYHEAD_SYMBOL.to_string(),
5015                Variable::new_from_typed_property(playhead.clone()),
5016            ),
5017        ]));
5018        let playhead_binding = Some(Box::new(ValueDefinition::Identifier(PaxIdentifier::new(
5019            TRANSITION_PLAYHEAD_SYMBOL,
5020        ))));
5021        let enter = TimelineTrackDefinition {
5022            elements: vec![
5023                TimelineTrackElement::Keyframe(TimelineKeyframe {
5024                    marker: TimelineMarker::Frame(0),
5025                    value: expression("$base - 100px"),
5026                    easing: Some(Token::new_without_location("InQuad".to_string())),
5027                }),
5028                TimelineTrackElement::Keyframe(TimelineKeyframe {
5029                    marker: TimelineMarker::Frame(10),
5030                    value: expression("$base"),
5031                    easing: None,
5032                }),
5033            ],
5034            playhead: playhead_binding,
5035            duration: Some(Box::new(ValueDefinition::LiteralValue(PaxValue::Duration(
5036                Duration::Frames(10.into()),
5037            )))),
5038            repeat: Some(false),
5039            starting_value: None,
5040            interruption: Default::default(),
5041            use_local_property_scope: false,
5042        };
5043        let transition = TransitionDefinition {
5044            enter: Some(enter),
5045            ..Default::default()
5046        };
5047        let columns = columns_for("y", vec![entry(ValueDefinition::Transition(transition))]);
5048
5049        let common = create_new_common_properties_from_columns(&columns, &stack);
5050        let y = common.borrow().y.get().unwrap();
5051        assert!((y.get_pixels(100.0) - -75.0).abs() < 0.0001);
5052
5053        playhead.set(10.0);
5054        let y = common.borrow().y.get().unwrap();
5055        assert!(y.get_pixels(100.0).abs() < 0.0001);
5056    }
5057
5058    #[test]
5059    fn base_symbol_uses_opaque_for_unset_opacity_transitions() {
5060        let phase = LocalProperty::new(TRANSITION_PHASE_ENTER);
5061        let playhead = LocalProperty::new(5.0_f64);
5062        let stack = RuntimePropertiesStackFrame::new(HashMap::from([
5063            (
5064                TRANSITION_PHASE_SYMBOL.to_string(),
5065                Variable::new_from_typed_property(phase.clone()),
5066            ),
5067            (
5068                TRANSITION_PLAYHEAD_SYMBOL.to_string(),
5069                Variable::new_from_typed_property(playhead.clone()),
5070            ),
5071        ]));
5072        let playhead_binding = Some(Box::new(ValueDefinition::Identifier(PaxIdentifier::new(
5073            TRANSITION_PLAYHEAD_SYMBOL,
5074        ))));
5075        let enter = TimelineTrackDefinition {
5076            elements: vec![
5077                TimelineTrackElement::Keyframe(TimelineKeyframe {
5078                    marker: TimelineMarker::Frame(0),
5079                    value: ValueDefinition::LiteralValue(PaxValue::Numeric(0.0.into())),
5080                    easing: Some(Token::new_without_location("Linear".to_string())),
5081                }),
5082                TimelineTrackElement::Keyframe(TimelineKeyframe {
5083                    marker: TimelineMarker::Frame(10),
5084                    value: expression("$base"),
5085                    easing: None,
5086                }),
5087            ],
5088            playhead: playhead_binding,
5089            duration: Some(Box::new(ValueDefinition::LiteralValue(PaxValue::Duration(
5090                Duration::Frames(10.into()),
5091            )))),
5092            repeat: Some(false),
5093            starting_value: None,
5094            interruption: Default::default(),
5095            use_local_property_scope: false,
5096        };
5097        let transition = TransitionDefinition {
5098            enter: Some(enter),
5099            ..Default::default()
5100        };
5101        let columns = columns_for(
5102            "opacity",
5103            vec![entry(ValueDefinition::Transition(transition))],
5104        );
5105
5106        let common = create_new_common_properties_from_columns(&columns, &stack);
5107        assert_eq!(
5108            common.borrow().opacity.get(),
5109            Some(Opacity::Alpha(0.5.into()))
5110        );
5111
5112        playhead.set(10.0);
5113        assert_eq!(
5114            common.borrow().opacity.get(),
5115            Some(Opacity::Alpha(1.0.into()))
5116        );
5117    }
5118
5119    #[test]
5120    fn scalar_common_property_alias_applies_to_both_axes() {
5121        let stack = empty_stack();
5122        let defined = BTreeMap::from([("scale".to_string(), percent_literal(20.0))]);
5123        let columns = property_columns_from_defined_properties(&defined);
5124
5125        let common = create_new_common_properties_from_columns(&columns, &stack);
5126
5127        assert_percent(common.borrow().scale_x.get(), 20.0);
5128        assert_percent(common.borrow().scale_y.get(), 20.0);
5129    }
5130
5131    #[test]
5132    fn list_common_property_alias_splits_across_axes() {
5133        let stack = empty_stack();
5134        let defined = BTreeMap::from([("anchor".to_string(), size_pair_literal(25.0, 75.0))]);
5135        let columns = property_columns_from_defined_properties(&defined);
5136
5137        let common = create_new_common_properties_from_columns(&columns, &stack);
5138
5139        assert_percent(common.borrow().anchor_x.get(), 25.0);
5140        assert_percent(common.borrow().anchor_y.get(), 75.0);
5141    }
5142
5143    #[test]
5144    fn axis_specific_common_property_overrides_alias_with_base() {
5145        let stack = empty_stack();
5146        let defined = BTreeMap::from([
5147            ("scale".to_string(), percent_literal(20.0)),
5148            ("scale_y".to_string(), expression("$base + 5")),
5149        ]);
5150        let columns = property_columns_from_defined_properties(&defined);
5151
5152        let common = create_new_common_properties_from_columns(&columns, &stack);
5153
5154        assert_percent(common.borrow().scale_x.get(), 20.0);
5155        assert_percent(common.borrow().scale_y.get(), 25.0);
5156    }
5157}
5158
5159/// Applies resolved common-property columns to an existing expanded node,
5160/// preserving layer order so `$base` can reference each prior layer.
5161pub fn update_existing_common_properties_from_columns(
5162    expanded_node: &Rc<ExpandedNode>,
5163    property_columns: &RuntimeResolvedPropertyColumns,
5164    stack_frame: &Rc<RuntimePropertiesStackFrame>,
5165) {
5166    let outer_ref = expanded_node.common_properties.borrow();
5167    let rc = Rc::clone(&outer_ref);
5168    let inner_ref = (*rc).borrow_mut();
5169    let mut cp = inner_ref;
5170
5171    update_common_properties(&mut cp, property_columns, stack_frame, Some(expanded_node));
5172}
5173
5174/// Applies a flattened common-property map to an existing expanded node.
5175pub fn update_existing_common_properties(
5176    expanded_node: &Rc<ExpandedNode>,
5177    defined_properties: &BTreeMap<String, pax_manifest::ValueDefinition>,
5178    stack_frame: &Rc<RuntimePropertiesStackFrame>,
5179) {
5180    update_existing_common_properties_from_columns(
5181        expanded_node,
5182        &property_columns_from_defined_properties(defined_properties),
5183        stack_frame,
5184    );
5185}
5186
5187fn create_id_property(
5188    property_columns: &RuntimeResolvedPropertyColumns,
5189) -> LocalProperty<Option<String>> {
5190    let id = property_columns
5191        .get("id")
5192        .and_then(|entries| entries.last());
5193    LocalProperty::new(
5194        if let Some(RuntimeResolvedPropertyEntry {
5195            value: pax_manifest::ValueDefinition::Identifier(pax_identifier),
5196            ..
5197        }) = id
5198        {
5199            Some(pax_identifier.name.clone())
5200        } else {
5201            None
5202        },
5203    )
5204}
5205
5206/// Creates common properties from resolved columns, preserving layer order so
5207/// `$base` can reference each prior layer.
5208pub fn create_new_common_properties_from_columns(
5209    property_columns: &RuntimeResolvedPropertyColumns,
5210    stack_frame: &Rc<RuntimePropertiesStackFrame>,
5211) -> Rc<RefCell<CommonProperties>> {
5212    Rc::new(RefCell::new(CommonProperties {
5213        id: create_id_property(property_columns),
5214        x: resolve_property("x", property_columns, stack_frame),
5215        y: resolve_property("y", property_columns, stack_frame),
5216        padding_x: resolve_property("padding_x", property_columns, stack_frame),
5217        padding_y: resolve_property("padding_y", property_columns, stack_frame),
5218        width: resolve_property("width", property_columns, stack_frame),
5219        height: resolve_property("height", property_columns, stack_frame),
5220        scale_x: resolve_property("scale_x", property_columns, stack_frame),
5221        scale_y: resolve_property("scale_y", property_columns, stack_frame),
5222        skew_x: resolve_property("skew_x", property_columns, stack_frame),
5223        skew_y: resolve_property("skew_y", property_columns, stack_frame),
5224        rotate: resolve_property("rotate", property_columns, stack_frame),
5225        transform: resolve_property("transform", property_columns, stack_frame),
5226        opacity: resolve_property("opacity", property_columns, stack_frame),
5227        layout_role: resolve_property("layout_role", property_columns, stack_frame),
5228        anchor_x: resolve_property("anchor_x", property_columns, stack_frame),
5229        anchor_y: resolve_property("anchor_y", property_columns, stack_frame),
5230        unclippable: resolve_property("unclippable", property_columns, stack_frame),
5231        _raycastable: resolve_property("_raycastable", property_columns, stack_frame),
5232        _suspended: resolve_property("_suspended", property_columns, stack_frame),
5233    }))
5234}
5235
5236/// Creates common properties from a flattened property map.
5237pub fn create_new_common_properties(
5238    defined_properties: &BTreeMap<String, pax_manifest::ValueDefinition>,
5239    stack_frame: &Rc<RuntimePropertiesStackFrame>,
5240) -> Rc<RefCell<CommonProperties>> {
5241    create_new_common_properties_from_columns(
5242        &property_columns_from_defined_properties(defined_properties),
5243        stack_frame,
5244    )
5245}
5246
5247fn bind_common_setting<T: CoercionRules + PropertyValue + ToPaxValue>(
5248    output: &LocalProperty<Option<T>>,
5249    name: &str,
5250    columns: &RuntimeResolvedPropertyColumns,
5251    stack: &Rc<RuntimePropertiesStackFrame>,
5252    node: Option<&ExpandedNode>,
5253) {
5254    let target = resolve_property(name, columns, stack);
5255    let fallback = common_property_base_fallback::<T>(name);
5256    let has_numeric_fallback = matches!(
5257        name,
5258        "x" | "y"
5259            | "anchor_x"
5260            | "anchor_y"
5261            | "rotate"
5262            | "skew_x"
5263            | "skew_y"
5264            | "opacity"
5265            | "padding_x"
5266            | "padding_y"
5267            | "scale_x"
5268            | "scale_y"
5269    );
5270    crate::bind_settings_property_with(
5271        output,
5272        target,
5273        &format!("common:{name}"),
5274        columns.get(name).map(Vec::as_slice).unwrap_or_default(),
5275        stack,
5276        node,
5277        move |from, to, t| match (from, to) {
5278            (Some(from), Some(to)) => Some(from.interpolate(to, t)),
5279            _ if has_numeric_fallback => Some(
5280                from.as_ref()
5281                    .unwrap_or(&fallback)
5282                    .interpolate(to.as_ref().unwrap_or(&fallback), t),
5283            ),
5284            _ => to.clone(),
5285        },
5286    );
5287}
5288
5289fn update_common_properties(
5290    cp: &mut CommonProperties,
5291    property_columns: &RuntimeResolvedPropertyColumns,
5292    stack_frame: &Rc<RuntimePropertiesStackFrame>,
5293    node: Option<&ExpandedNode>,
5294) {
5295    cp.id.replace_with(create_id_property(property_columns));
5296    bind_common_setting(&cp.x, "x", property_columns, stack_frame, node);
5297    bind_common_setting(&cp.y, "y", property_columns, stack_frame, node);
5298    bind_common_setting(
5299        &cp.padding_x,
5300        "padding_x",
5301        property_columns,
5302        stack_frame,
5303        node,
5304    );
5305    bind_common_setting(
5306        &cp.padding_y,
5307        "padding_y",
5308        property_columns,
5309        stack_frame,
5310        node,
5311    );
5312    bind_common_setting(&cp.width, "width", property_columns, stack_frame, node);
5313    bind_common_setting(&cp.height, "height", property_columns, stack_frame, node);
5314    bind_common_setting(&cp.scale_x, "scale_x", property_columns, stack_frame, node);
5315    bind_common_setting(&cp.scale_y, "scale_y", property_columns, stack_frame, node);
5316    bind_common_setting(&cp.skew_x, "skew_x", property_columns, stack_frame, node);
5317    bind_common_setting(&cp.skew_y, "skew_y", property_columns, stack_frame, node);
5318    bind_common_setting(&cp.rotate, "rotate", property_columns, stack_frame, node);
5319    bind_common_setting(
5320        &cp.transform,
5321        "transform",
5322        property_columns,
5323        stack_frame,
5324        node,
5325    );
5326    bind_common_setting(&cp.opacity, "opacity", property_columns, stack_frame, node);
5327    bind_common_setting(
5328        &cp.layout_role,
5329        "layout_role",
5330        property_columns,
5331        stack_frame,
5332        node,
5333    );
5334    bind_common_setting(
5335        &cp.anchor_x,
5336        "anchor_x",
5337        property_columns,
5338        stack_frame,
5339        node,
5340    );
5341    bind_common_setting(
5342        &cp.anchor_y,
5343        "anchor_y",
5344        property_columns,
5345        stack_frame,
5346        node,
5347    );
5348    bind_common_setting(
5349        &cp.unclippable,
5350        "unclippable",
5351        property_columns,
5352        stack_frame,
5353        node,
5354    );
5355    bind_common_setting(
5356        &cp._raycastable,
5357        "_raycastable",
5358        property_columns,
5359        stack_frame,
5360        node,
5361    );
5362    bind_common_setting(
5363        &cp._suspended,
5364        "_suspended",
5365        property_columns,
5366        stack_frame,
5367        node,
5368    );
5369}
5370
5371#[cfg(test)]
5372mod runtime_settings_tests {
5373    use super::{add_removed_property_resets, resolve_runtime_settings_with_layers_for_node};
5374    use crate::{
5375        ExpandedNodeIdentifier, RuntimePropertiesStackFrame, RuntimeSettingsLayer,
5376        RuntimeSettingsSource,
5377    };
5378    use pax_manifest::{
5379        LiteralBlockDefinition, PaxIdentifier, SettingElement, SettingsBlockElement,
5380        TemplateNodeDefinition, TimelineBlockElement, TimelineDefinition,
5381        TimelineSelectorBlockDefinition, TimelineSelectorElement, TimelineTrackDefinition, Token,
5382        TypeId, ValueDefinition,
5383    };
5384    use pax_runtime_api::PaxValue;
5385    use std::collections::{BTreeMap, HashMap};
5386
5387    fn setting(name: &str, value: i32) -> SettingElement {
5388        SettingElement::Setting(
5389            Token::new_without_location(name.to_string()),
5390            ValueDefinition::LiteralValue(PaxValue::Numeric(value.into())),
5391        )
5392    }
5393
5394    fn selector_block(selector: &str, elements: Vec<SettingElement>) -> SettingsBlockElement {
5395        SettingsBlockElement::SelectorBlock(
5396            Token::new_without_location(selector.to_string()),
5397            LiteralBlockDefinition::new(elements),
5398        )
5399    }
5400
5401    #[test]
5402    fn runtime_settings_resolve_columns_and_provenance_by_layer() {
5403        let mut node = TemplateNodeDefinition {
5404            type_id: TypeId::build_singleton("example::Text", Some("Text")),
5405            control_flow_settings: None,
5406            settings: Some(vec![
5407                SettingElement::Setting(
5408                    Token::new_without_location("id".to_string()),
5409                    ValueDefinition::Identifier(PaxIdentifier::new("hero")),
5410                ),
5411                setting("fill", 4),
5412            ]),
5413            selector_info: pax_manifest::TemplateNodeSelectorInfo {
5414                class_binding: Some(ValueDefinition::LiteralValue(PaxValue::String(
5415                    "headline".to_string(),
5416                ))),
5417                ..Default::default()
5418            },
5419            raw_comment_string: None,
5420        };
5421        node.normalize_selector_info();
5422
5423        let component_settings = Some(vec![selector_block(
5424            "Text",
5425            vec![setting("width", 1), setting("fill", 1)],
5426        )]);
5427        let imported_layers = vec![
5428            RuntimeSettingsLayer {
5429                transition: None,
5430                provider_id: ExpandedNodeIdentifier(10),
5431                provider_type_id: TypeId::build_singleton("example::BaseTheme", Some("BaseTheme")),
5432                provider_stack: RuntimePropertiesStackFrame::new(HashMap::new()),
5433                settings: vec![selector_block(
5434                    "Text",
5435                    vec![setting("height", 2), setting("fill", 2)],
5436                )],
5437            },
5438            RuntimeSettingsLayer {
5439                transition: None,
5440                provider_id: ExpandedNodeIdentifier(11),
5441                provider_type_id: TypeId::build_singleton(
5442                    "example::AccentTheme",
5443                    Some("AccentTheme"),
5444                ),
5445                provider_stack: RuntimePropertiesStackFrame::new(HashMap::new()),
5446                settings: vec![selector_block(
5447                    ".headline",
5448                    vec![setting("opacity", 3), setting("fill", 3)],
5449                )],
5450            },
5451        ];
5452
5453        let resolved = resolve_runtime_settings_with_layers_for_node(
5454            &node,
5455            &BTreeMap::new(),
5456            &component_settings,
5457            &imported_layers,
5458            &["headline".to_string()],
5459            None,
5460            &[],
5461        );
5462
5463        assert!(matches!(
5464            resolved.defined_properties.get("width"),
5465            Some(ValueDefinition::LiteralValue(PaxValue::Numeric(value))) if *value == 1.into()
5466        ));
5467        assert!(matches!(
5468            resolved.defined_properties.get("height"),
5469            Some(ValueDefinition::LiteralValue(PaxValue::Numeric(value))) if *value == 2.into()
5470        ));
5471        assert!(matches!(
5472            resolved.defined_properties.get("opacity"),
5473            Some(ValueDefinition::LiteralValue(PaxValue::Numeric(value))) if *value == 3.into()
5474        ));
5475        assert!(matches!(
5476            resolved.defined_properties.get("fill"),
5477            Some(ValueDefinition::LiteralValue(PaxValue::Numeric(value))) if *value == 4.into()
5478        ));
5479
5480        let fill_column = resolved
5481            .columns
5482            .get("fill")
5483            .expect("fill column should exist");
5484        assert_eq!(fill_column.len(), 4);
5485        assert!(matches!(
5486            fill_column[0].source,
5487            RuntimeSettingsSource::ComponentSettings
5488        ));
5489        assert!(matches!(
5490            fill_column[1].source,
5491            RuntimeSettingsSource::ImportedLayer { provider_id, .. } if provider_id == ExpandedNodeIdentifier(10)
5492        ));
5493        assert!(matches!(
5494            fill_column[2].source,
5495            RuntimeSettingsSource::ImportedLayer { provider_id, .. } if provider_id == ExpandedNodeIdentifier(11)
5496        ));
5497        assert!(matches!(
5498            fill_column[3].source,
5499            RuntimeSettingsSource::Inline
5500        ));
5501
5502        let width_source = resolved
5503            .provenance
5504            .get("width")
5505            .expect("width provenance should exist");
5506        assert!(matches!(
5507            width_source.source,
5508            RuntimeSettingsSource::ComponentSettings
5509        ));
5510        assert!(matches!(
5511            width_source.selector,
5512            Some(pax_manifest::SelectorExpr::Type(ref selector)) if selector == "Text"
5513        ));
5514
5515        let opacity_source = resolved
5516            .provenance
5517            .get("opacity")
5518            .expect("opacity provenance should exist");
5519        assert!(matches!(
5520            opacity_source.source,
5521            RuntimeSettingsSource::ImportedLayer { provider_id, .. } if provider_id == ExpandedNodeIdentifier(11)
5522        ));
5523        assert!(matches!(
5524            opacity_source.selector,
5525            Some(pax_manifest::SelectorExpr::Class(ref selector)) if selector == "headline"
5526        ));
5527
5528        let fill_source = resolved
5529            .provenance
5530            .get("fill")
5531            .expect("fill provenance should exist");
5532        assert!(matches!(fill_source.source, RuntimeSettingsSource::Inline));
5533        assert!(fill_source.selector.is_none());
5534    }
5535
5536    #[test]
5537    fn selector_cascade_is_type_then_classes_then_id_then_inline() {
5538        let mut node = TemplateNodeDefinition {
5539            type_id: TypeId::build_singleton("example::Text", Some("Text")),
5540            settings: Some(vec![
5541                SettingElement::Setting(
5542                    Token::new_without_location("id".to_string()),
5543                    ValueDefinition::Identifier(PaxIdentifier::new("hero")),
5544                ),
5545                setting("fill", 5),
5546            ]),
5547            ..Default::default()
5548        };
5549        node.normalize_selector_info();
5550        let component_settings = Some(vec![
5551            selector_block("Text", vec![setting("fill", 1)]),
5552            selector_block(".base", vec![setting("fill", 2)]),
5553            selector_block(".selected", vec![setting("fill", 3)]),
5554            selector_block("#hero", vec![setting("fill", 4)]),
5555        ]);
5556
5557        let resolved = resolve_runtime_settings_with_layers_for_node(
5558            &node,
5559            &BTreeMap::new(),
5560            &component_settings,
5561            &[],
5562            &["base".to_string(), "selected".to_string()],
5563            None,
5564            &[],
5565        );
5566        let values = resolved.columns["fill"]
5567            .iter()
5568            .map(|entry| match &entry.value {
5569                ValueDefinition::LiteralValue(PaxValue::Numeric(value)) => *value,
5570                other => panic!("unexpected fill value: {other:?}"),
5571            })
5572            .collect::<Vec<_>>();
5573
5574        assert_eq!(
5575            values,
5576            vec![1.into(), 2.into(), 3.into(), 4.into(), 5.into()]
5577        );
5578    }
5579
5580    #[test]
5581    fn lifecycle_timeline_uses_classes_captured_at_transition_start() {
5582        let node = TemplateNodeDefinition {
5583            type_id: TypeId::build_singleton("example::Rectangle", Some("Rectangle")),
5584            ..Default::default()
5585        };
5586        let component_settings = Some(vec![SettingsBlockElement::Transition(
5587            Token::new_without_location("in".to_string()),
5588            Token::new_without_location("appear".to_string()),
5589        )]);
5590        let timelines = vec![TimelineDefinition {
5591            name: Some(Token::new_without_location("appear".to_string())),
5592            elements: vec![TimelineBlockElement::SelectorBlock(
5593                Token::new_without_location(".active".to_string()),
5594                TimelineSelectorBlockDefinition {
5595                    elements: vec![TimelineSelectorElement::Track(
5596                        Token::new_without_location("opacity".to_string()),
5597                        TimelineTrackDefinition::default(),
5598                    )],
5599                },
5600            )],
5601            ..Default::default()
5602        }];
5603
5604        let resolved = resolve_runtime_settings_with_layers_for_node(
5605            &node,
5606            &BTreeMap::new(),
5607            &component_settings,
5608            &[],
5609            &[],
5610            Some(&["active".to_string()]),
5611            &timelines,
5612        );
5613
5614        assert!(matches!(
5615            resolved.defined_properties.get("opacity"),
5616            Some(ValueDefinition::Transition(_))
5617        ));
5618    }
5619
5620    #[test]
5621    fn removed_runtime_properties_are_explicitly_reset_only_when_requested() {
5622        let previous =
5623            BTreeMap::from([("fill".to_string(), vec![]), ("width".to_string(), vec![])]);
5624        let mut current = BTreeMap::from([("width".to_string(), vec![])]);
5625
5626        add_removed_property_resets(&previous, &mut current);
5627
5628        assert!(current.contains_key("fill"));
5629        assert!(current.contains_key("width"));
5630    }
5631}