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