1use std::cell::{Cell, RefCell};
2use std::collections::{BTreeMap, BTreeSet};
3use std::fmt;
4use std::rc::Rc;
5use std::sync::Arc;
6use std::sync::atomic::{AtomicU64, Ordering};
7use std::time::{Duration, Instant};
8
9use rhai::{
10 AST, ASTNode, Array, Dynamic, Engine, EvalAltResult, Expr, FnPtr, FuncArgs, FuncRegistration,
11 ImmutableString, Map, Module, ModuleResolver, Position, Scope, Stmt,
12};
13use serde::{Deserialize, Serialize};
14use thiserror::Error;
15
16use crate::asset::{AssetId, ImageDecodeHandle, asset_id_from_script};
17use crate::backend::{AstInterpreter, ExecutionBackend};
18use crate::canvas::register_canvas_api;
19use crate::column_resize::{
20 ColumnMeasurementRegistry, ColumnResizePrimitiveHandler, IntrinsicTextMeasurePrimitiveHandler,
21 column_resize_primitive_descriptor, intrinsic_text_measure_primitive_descriptor,
22};
23use crate::component::{ComponentExportCollector, ComponentExportError, ComponentRegistry};
24use crate::context::{UiContext, register_ui_context_api};
25use crate::date::register_date_api;
26use crate::drag_drop::{
27 DragSourcePrimitiveHandler, DropZonePrimitiveHandler, drag_source_primitive_descriptor,
28 drop_zone_primitive_descriptor,
29};
30use crate::draggable::{DraggablePrimitiveHandler, draggable_primitive_descriptor};
31use crate::motion::register_motion_api;
32use crate::node::{
33 asset_image_node, box_node, canvas_node, column_node, directional_asset_image_node,
34 directional_image_node, error_boundary_node, fragment_node, generic_directional_image_node,
35 generic_image_node, image_node, layer_node, lazy_error_boundary_node, motion_group_node,
36 overlay_node, rich_text_node, row_node, span_value, stack_node, svg_node, text_node,
37};
38use crate::pan_zoom::{PanZoomPrimitiveHandler, pan_zoom_primitive_descriptor};
39use crate::primitive::{PrimitiveDescriptor, PrimitiveError, PrimitiveHandler, PrimitiveRegistry};
40use crate::range_input::{RangeInputPrimitiveHandler, range_input_primitive_descriptor};
41use crate::range_slider::{RangeSliderPrimitiveHandler, range_slider_primitive_descriptor};
42use crate::resizable::{ResizablePrimitiveHandler, resizable_primitive_descriptor};
43use crate::rotatable::{RotatablePrimitiveHandler, rotatable_primitive_descriptor};
44use crate::selection_area::{SelectionAreaPrimitiveHandler, selection_area_primitive_descriptor};
45use crate::sortable::{SortablePrimitiveHandler, sortable_primitive_descriptor};
46use crate::split_resize::{SplitResizePrimitiveHandler, split_resize_primitive_descriptor};
47use crate::style::register_style_api;
48use crate::text_area::{
49 TextAreaPrimitiveHandler, register_text_area_api, text_area_primitive_descriptor,
50};
51use crate::text_input::{TextInputPrimitiveHandler, text_input_primitive_descriptor};
52use crate::value::OpaqueHandle;
53use crate::{
54 ComponentInstancePath, ComponentStateSchema, EventSchema, ExecutionPhase, ModuleId,
55 RenderStateTransaction, UiNode, UiRuntimeState, UiValue,
56};
57use crate::{SubscriptionHandle, TaskHandle};
58
59#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
60pub struct ScriptGeneration(u64);
61
62impl ScriptGeneration {
63 #[must_use]
64 pub const fn get(self) -> u64 {
65 self.0
66 }
67
68 #[cfg(test)]
69 #[must_use]
70 pub(crate) fn initial() -> Self {
71 Self(1)
72 }
73
74 #[cfg(test)]
75 #[must_use]
76 pub(crate) fn next(self) -> Self {
77 Self(self.0.saturating_add(1))
78 }
79}
80
81impl fmt::Display for ScriptGeneration {
82 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
83 self.0.fmt(formatter)
84 }
85}
86
87#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
88#[serde(rename_all = "snake_case")]
89pub enum ExecutionOperation {
90 Compile,
91 Render,
92 ComponentReuse(usize),
96 VirtualCollection(String),
97 Lifecycle(String),
98 Callback(String),
99}
100
101#[derive(Clone, Debug, Eq, PartialEq)]
102pub struct ExecutionTiming {
103 pub operation: ExecutionOperation,
104 pub source: String,
105 pub duration: Duration,
106 pub operations: u64,
107 pub operation_limit: u64,
109 pub round_operations: u64,
112 pub operation_semantics: u32,
113 pub slow: bool,
114 pub succeeded: bool,
115}
116
117pub const OPERATION_SEMANTICS_VERSION: u32 = 2;
118
119#[derive(Debug, Error)]
120pub enum RuntimeError {
121 #[error("failed to compile Rhai UI source: {0}")]
122 Compile(#[source] Box<EvalAltResult>),
123 #[error("failed to evaluate Rhai UI source: {0}")]
124 Evaluate(#[source] Box<EvalAltResult>),
125 #[error("callback `{name}` is invalid: {source}")]
126 CallbackDefinition {
127 name: String,
128 #[source]
129 source: Box<EvalAltResult>,
130 },
131 #[error("callback `{name}` cannot be retained: {source}")]
132 RetainedCallback {
133 name: String,
134 source: ScriptCallbackDefinitionError,
135 },
136 #[error(
137 "callback `{name}` belongs to script generation {callback_generation}, but current generation is {current_generation}"
138 )]
139 StaleCallback {
140 name: String,
141 callback_generation: ScriptGeneration,
142 current_generation: ScriptGeneration,
143 },
144 #[error("callback `{name}` belongs to an unmounted incarnation of component `{component}`")]
145 StaleComponentCallback {
146 name: String,
147 component: ComponentInstancePath,
148 },
149 #[error("component runtime failed: {0}")]
150 ComponentRuntime(String),
151 #[error("component invocation `{0}` is not retained in the active generation")]
152 MissingComponentInvocation(ComponentInstancePath),
153 #[error("invalid script import: {0}")]
154 Import(String),
155 #[error(
156 "compiled Rhai source contains an assignment target that Rhai 1.26 cannot evaluate safely at {0}"
157 )]
158 InvalidAssignmentTarget(Position),
159}
160
161#[derive(Debug, Error)]
162pub enum ScriptCallbackDefinitionError {
163 #[error("anonymous or capturing functions cannot escape one synchronous Rhai evaluation")]
164 Anonymous,
165 #[error("curried argument {index} cannot cross the retained callback boundary: {source}")]
166 InvalidCurry {
167 index: usize,
168 source: crate::UiValueError,
169 },
170}
171
172#[derive(Clone, Debug)]
178struct ComponentCallbackBinding {
179 component: ComponentInstancePath,
180 incarnation: crate::ComponentIncarnation,
181 events: BTreeMap<String, EventSchema>,
182 context: Option<crate::invocation::ScriptInvocationContext>,
183}
184
185#[derive(Clone)]
186pub struct CompiledUi {
187 ast: AST,
188 generation: ScriptGeneration,
189}
190
191#[derive(Clone, Debug)]
192pub struct ScriptCallback {
193 function: FnPtr,
194 curry: Vec<UiValue>,
195 generation: ScriptGeneration,
196 component: Option<ComponentInstancePath>,
197 incarnation: Option<crate::ComponentIncarnation>,
198 events: BTreeMap<String, EventSchema>,
199 #[allow(deprecated)]
200 native_context: Option<crate::invocation::ScriptInvocationContext>,
201}
202
203impl PartialEq for ScriptCallback {
204 fn eq(&self, other: &Self) -> bool {
205 self.name() == other.name()
206 && self.curry == other.curry
207 && self.generation == other.generation
208 && self.component == other.component
209 && self.incarnation == other.incarnation
210 }
211}
212
213impl Eq for ScriptCallback {}
214
215impl ScriptCallback {
216 #[must_use]
217 pub fn name(&self) -> &str {
218 self.function.fn_name()
219 }
220
221 #[must_use]
222 pub fn generation(&self) -> ScriptGeneration {
223 self.generation
224 }
225
226 pub(crate) fn try_from_fn_ptr(
227 mut function: FnPtr,
228 generation: ScriptGeneration,
229 ) -> Result<Self, ScriptCallbackDefinitionError> {
230 if function.is_anonymous() {
231 return Err(ScriptCallbackDefinitionError::Anonymous);
232 }
233 let mut binding = None;
234 let mut curry = Vec::new();
235 for (index, value) in function.iter_curry().cloned().enumerate() {
236 if value.is::<ComponentCallbackBinding>() {
237 binding = Some(value.cast::<ComponentCallbackBinding>());
238 continue;
239 }
240 curry.push(
241 UiValue::from_dynamic(value).map_err(|source| {
242 ScriptCallbackDefinitionError::InvalidCurry { index, source }
243 })?,
244 );
245 }
246 function.set_curry(curry.iter().cloned().map(UiValue::into_dynamic));
247 let (component, incarnation, events, native_context) = binding.map_or_else(
248 || (None, None, BTreeMap::new(), None),
249 |binding| {
250 (
251 Some(binding.component),
252 Some(binding.incarnation),
253 binding.events,
254 binding.context,
255 )
256 },
257 );
258 Ok(Self {
259 function,
260 curry,
261 generation,
262 component,
263 incarnation,
264 events,
265 native_context,
266 })
267 }
268
269 pub(crate) fn bind_generation(&mut self, generation: ScriptGeneration) {
270 self.generation = generation;
271 }
272
273 #[cfg(test)]
274 pub(crate) fn bind_component_if_unset(
275 &mut self,
276 component: ComponentInstancePath,
277 events: BTreeMap<String, EventSchema>,
278 ) {
279 if self.component.is_none() {
280 self.component = Some(component);
281 self.events = events;
282 }
283 }
284
285 pub(crate) fn bind_component_scope_if_unset(
286 &mut self,
287 component: &ComponentInstancePath,
288 incarnation: crate::ComponentIncarnation,
289 events: BTreeMap<String, EventSchema>,
290 ) {
291 if self.component.is_none() {
292 self.component = Some(component.clone());
293 self.events = events;
294 }
295 if self.component.as_ref() == Some(component) && self.incarnation.is_none() {
296 self.incarnation = Some(incarnation);
297 }
298 }
299
300 pub(crate) fn bind_native_context_if_unset(
301 &mut self,
302 context: crate::invocation::ScriptInvocationContext,
303 ) {
304 if self.native_context.is_none() {
305 self.native_context = Some(context);
306 }
307 }
308
309 pub(crate) fn component(&self) -> Option<&ComponentInstancePath> {
310 self.component.as_ref()
311 }
312
313 pub(crate) const fn incarnation(&self) -> Option<crate::ComponentIncarnation> {
314 self.incarnation
315 }
316
317 pub(crate) fn events(&self) -> &BTreeMap<String, EventSchema> {
318 &self.events
319 }
320
321 pub(crate) fn native_context(&self) -> Option<&crate::invocation::ScriptInvocationContext> {
322 self.native_context.as_ref()
323 }
324}
325
326impl CompiledUi {
327 #[must_use]
328 pub fn generation(&self) -> ScriptGeneration {
329 self.generation
330 }
331
332 #[must_use]
333 pub fn has_function(&self, name: &str, parameter_count: usize) -> bool {
334 self.ast
335 .iter_functions()
336 .any(|function| function.name == name && function.params.len() == parameter_count)
337 }
338}
339
340struct ActiveComponentRender {
341 root_context: UiContext,
342 stack: Vec<ComponentInstancePath>,
343 contexts: Vec<UiContext>,
344 seen: BTreeSet<ComponentInstancePath>,
345 positions: BTreeMap<String, usize>,
346 transaction: RenderStateTransaction,
347 generation: ScriptGeneration,
348 state_snapshot: crate::StateStore,
349 event_handlers: BTreeMap<(ComponentInstancePath, String), ScriptCallback>,
350 invocations: BTreeMap<ComponentInstancePath, ComponentInvocationRecipe>,
351 effect_keys: Vec<Option<BTreeSet<String>>>,
352 effects: BTreeMap<crate::EffectId, crate::EffectDescriptor>,
353 timers: BTreeMap<crate::TimerId, crate::TimerDescriptor>,
354 signals: BTreeMap<crate::SignalId, crate::signal::SignalDescriptor>,
355 element_refs: BTreeSet<crate::ElementRefId>,
356 virtual_collections: BTreeMap<crate::VirtualCollectionId, VirtualCollectionRecipe>,
357 environment: ComponentRenderEnvironment,
358 reuse: Option<ComponentReuseSnapshot>,
359 reused: Vec<(String, usize)>,
360 failed_component: Option<ComponentInstancePath>,
361}
362
363#[derive(Clone, Copy, Debug, Eq, PartialEq)]
364struct ComponentRenderEnvironment {
365 theme_generation: Option<u64>,
366 component_style_generation: u64,
367 locale_generation: Option<u64>,
368 calendar_day: crate::GregorianDate,
369}
370
371#[derive(Clone)]
372struct ComponentReusePlan {
373 previous_root: Rc<UiNode>,
374 subtrees: crate::node::ComponentSubtreeIndex,
375 dirty: BTreeSet<ComponentInstancePath>,
376}
377
378impl ComponentReusePlan {
379 fn new(previous_root: Rc<UiNode>, dirty: BTreeSet<ComponentInstancePath>) -> Self {
380 let subtrees = crate::node::ComponentSubtreeIndex::new(&previous_root);
381 Self {
382 previous_root,
383 subtrees,
384 dirty,
385 }
386 }
387}
388
389#[derive(Clone)]
390struct ComponentReuseSnapshot {
391 previous_root: Rc<UiNode>,
392 subtrees: crate::node::ComponentSubtreeIndex,
393 dirty: BTreeSet<ComponentInstancePath>,
394 invocations: BTreeMap<ComponentInstancePath, ComponentInvocationRecipe>,
395 event_handlers: BTreeMap<(ComponentInstancePath, String), ScriptCallback>,
396 effects: BTreeMap<crate::EffectId, crate::EffectDescriptor>,
397 timers: BTreeMap<crate::TimerId, crate::TimerDescriptor>,
398 signals: BTreeMap<crate::SignalId, crate::signal::SignalDescriptor>,
399 element_refs: BTreeSet<crate::ElementRefId>,
400 virtual_collections: BTreeMap<crate::VirtualCollectionId, VirtualCollectionRecipe>,
401}
402
403struct ReusedComponentScope {
404 node: UiNode,
405 invocations: BTreeMap<ComponentInstancePath, ComponentInvocationRecipe>,
406 event_handlers: BTreeMap<(ComponentInstancePath, String), ScriptCallback>,
407 resources: ReusedComponentResources,
408}
409
410struct ReusedComponentResources {
411 effects: BTreeMap<crate::EffectId, crate::EffectDescriptor>,
412 timers: BTreeMap<crate::TimerId, crate::TimerDescriptor>,
413 signals: BTreeMap<crate::SignalId, crate::signal::SignalDescriptor>,
414 element_refs: BTreeSet<crate::ElementRefId>,
415 virtual_collections: BTreeMap<crate::VirtualCollectionId, VirtualCollectionRecipe>,
416}
417
418#[derive(Clone, Debug)]
419struct VirtualCollectionRecipe {
420 id: crate::VirtualCollectionId,
421 data: crate::VirtualCollectionData,
422 renderer: FnPtr,
423 renderer_context: crate::invocation::ScriptInvocationContext,
424 context: UiContext,
425 generation: ScriptGeneration,
426}
427
428type ActiveComponentRenderState = Rc<RefCell<Option<ActiveComponentRender>>>;
429
430#[derive(Clone)]
431struct PendingComponentCommit {
432 root: ComponentInstancePath,
433 previous: BTreeSet<ComponentInstancePath>,
434 retained_root: Option<ComponentInstancePath>,
435 transaction: RenderStateTransaction,
436 event_handlers: BTreeMap<(ComponentInstancePath, String), ScriptCallback>,
437}
438
439#[derive(Clone, Debug)]
440pub struct ComponentInvocationRecipe {
441 path: ComponentInstancePath,
442 parent: ComponentInstancePath,
443 component: ModuleId,
444 export: String,
445 declared_key: Option<String>,
446 props: crate::ComponentProps,
447 script_props: Map,
448 component_context: UiContext,
449 caller_context: UiContext,
450 part_styles: BTreeMap<String, crate::Style>,
451 event_callbacks: Vec<(String, FnPtr)>,
452 component_events: BTreeMap<String, EventSchema>,
453 declared_effects: BTreeSet<String>,
454 render: FnPtr,
455 snapshot: crate::node::ComponentOwnedSnapshot,
456 context: crate::invocation::ScriptInvocationContext,
457 generation: ScriptGeneration,
458 environment: ComponentRenderEnvironment,
459 reusable: bool,
460}
461
462#[derive(Clone)]
463pub(crate) struct RegisteredComponentRender {
464 generation: ScriptGeneration,
465 render: FnPtr,
466}
467
468type ComponentRenderRegistry = Rc<RefCell<BTreeMap<ModuleId, RegisteredComponentRender>>>;
469
470impl ComponentInvocationRecipe {
471 #[must_use]
472 pub fn path(&self) -> &ComponentInstancePath {
473 &self.path
474 }
475
476 #[must_use]
477 pub fn parent(&self) -> &ComponentInstancePath {
478 &self.parent
479 }
480
481 #[must_use]
482 pub fn component(&self) -> &ModuleId {
483 &self.component
484 }
485
486 #[must_use]
487 pub fn export(&self) -> &str {
488 &self.export
489 }
490
491 #[must_use]
492 pub fn declared_key(&self) -> Option<&str> {
493 self.declared_key.as_deref()
494 }
495
496 #[must_use]
497 pub fn props(&self) -> &crate::ComponentProps {
498 &self.props
499 }
500
501 #[must_use]
502 pub fn render_name(&self) -> &str {
503 self.render.fn_name()
504 }
505
506 #[must_use]
507 pub fn has_invocation_context(&self) -> bool {
508 let _ = &self.context;
509 true
510 }
511
512 #[must_use]
513 pub fn generation(&self) -> ScriptGeneration {
514 self.generation
515 }
516}
517
518pub struct RuntimeEngine {
519 engine: Engine,
520 generation: ScriptGeneration,
521 preparation_generation: Option<ScriptGeneration>,
522 component_exports: ComponentExportCollector,
523 evaluation_generation: Rc<Cell<ScriptGeneration>>,
524 primitives: PrimitiveRegistry,
525 primitive_modules: BTreeMap<String, Module>,
526 timings: RefCell<Vec<ExecutionTiming>>,
527 last_failed_timing: RefCell<Option<ExecutionTiming>>,
528 last_failed_component: RefCell<Option<ComponentInstancePath>>,
529 operation_tracker: Rc<OperationTracker>,
530 operation_limit: Rc<Cell<u64>>,
531 slow_threshold: Duration,
532 component_render: ActiveComponentRenderState,
533 component_invocations: BTreeMap<ComponentInstancePath, ComponentInvocationRecipe>,
534 component_effects: BTreeMap<crate::EffectId, crate::EffectDescriptor>,
535 component_timers: BTreeMap<crate::TimerId, crate::TimerDescriptor>,
536 component_signals: BTreeMap<crate::SignalId, crate::signal::SignalDescriptor>,
537 component_element_refs: BTreeSet<crate::ElementRefId>,
538 pending_component_commits: BTreeMap<ComponentInstancePath, PendingComponentCommit>,
539 component_renderers: ComponentRenderRegistry,
540 native_handlers: crate::NativeHandlerRegistry,
541 syntax_registry: crate::SyntaxRegistry,
542 document_runtime: crate::DocumentRuntimeConfig,
543 #[cfg(feature = "charts")]
544 chart_transforms: crate::ChartTransformRegistry,
545 #[cfg(feature = "charts")]
546 chart_geo: crate::ChartGeoRegistry,
547 #[cfg(feature = "charts")]
548 chart_series: crate::ChartSeriesRegistry,
549 #[cfg(feature = "charts")]
550 chart_formatters: crate::ChartFormatterRegistry,
551 virtual_collections: BTreeMap<crate::VirtualCollectionId, VirtualCollectionRecipe>,
552}
553
554pub const DEFAULT_SCRIPT_OPERATION_LIMIT: u64 = 1_000_000;
555
556#[derive(Debug, Default)]
557struct OperationTracker {
558 last_absolute: Cell<u64>,
559 total: Cell<u64>,
560}
561
562impl OperationTracker {
563 fn begin(&self, inherited_base: u64) {
564 self.last_absolute.set(inherited_base);
565 self.total.set(0);
566 }
567
568 fn observe(&self, absolute: u64) -> u64 {
569 let last_absolute = self.last_absolute.get();
570 let delta = if absolute >= last_absolute {
571 absolute - last_absolute
572 } else {
573 1
577 };
578 let total = self.total.get().saturating_add(delta);
579 self.total.set(total);
580 self.last_absolute.set(absolute);
581 total
582 }
583
584 fn align(&self, inherited_base: u64) {
585 self.last_absolute.set(inherited_base);
590 }
591}
592
593#[derive(Clone, Copy)]
594struct ExecutionTimingStart {
595 instant: Instant,
596 operations: u64,
597 operation_limit: u64,
598}
599
600#[derive(Clone)]
601pub(crate) struct RuntimeEngineCheckpoint {
602 generation: ScriptGeneration,
603 evaluation_generation: ScriptGeneration,
604 component_invocations: BTreeMap<ComponentInstancePath, ComponentInvocationRecipe>,
605 component_effects: BTreeMap<crate::EffectId, crate::EffectDescriptor>,
606 component_timers: BTreeMap<crate::TimerId, crate::TimerDescriptor>,
607 component_signals: BTreeMap<crate::SignalId, crate::signal::SignalDescriptor>,
608 component_element_refs: BTreeSet<crate::ElementRefId>,
609 pending_component_commits: BTreeMap<ComponentInstancePath, PendingComponentCommit>,
610 virtual_collections: BTreeMap<crate::VirtualCollectionId, VirtualCollectionRecipe>,
611 component_snapshots: Vec<(crate::node::ComponentOwnedSnapshot, Option<Rc<UiNode>>)>,
612}
613
614impl Default for RuntimeEngine {
615 fn default() -> Self {
616 Self::new()
617 }
618}
619
620impl RuntimeEngine {
621 #[cfg(feature = "dev-reload")]
622 pub(crate) fn candidate_engine(&self) -> Self {
623 let mut candidate = Self::new();
624 candidate.slow_threshold = self.slow_threshold;
625 candidate.operation_limit.set(self.operation_limit.get());
626 let (global, functions) = self.expression_depth_limits();
627 candidate.set_expression_depth_limits(global, functions);
628 #[cfg(feature = "charts")]
629 {
630 candidate.chart_transforms.copy_from(&self.chart_transforms);
631 candidate.chart_geo.copy_from(&self.chart_geo);
632 candidate.chart_series.copy_from(&self.chart_series);
633 candidate.chart_formatters.copy_from(&self.chart_formatters);
634 }
635 candidate
636 }
637
638 #[must_use]
645 pub fn new() -> Self {
646 let mut engine = Engine::new();
647 engine.set_module_resolver(crate::source::RestrictedModuleResolver::new());
648 let operation_tracker = Rc::new(OperationTracker::default());
649 let progress = Rc::clone(&operation_tracker);
650 let operation_limit = Rc::new(Cell::new(DEFAULT_SCRIPT_OPERATION_LIMIT));
651 let limit_probe = Rc::clone(&operation_limit);
652 engine.on_progress(move |operations| {
653 let total = progress.observe(operations);
654 let limit = limit_probe.get();
655 (total > limit).then(|| {
656 Dynamic::from(format!(
657 "script operation budget exceeded: {total} > {limit}"
658 ))
659 })
660 });
661 engine.build_type::<UiNode>();
662 engine.build_type::<OpaqueHandle>();
663 engine.build_type::<TaskHandle>();
664 engine.build_type::<SubscriptionHandle>();
665 engine.build_type::<AssetId>();
666 engine.build_type::<ImageDecodeHandle>();
667 engine.build_type::<crate::NativeSignal>();
668 engine.build_type::<crate::EventResponse>();
669 engine.build_type::<crate::ElementRef>();
670 engine.build_type::<crate::NativeHandlerRef>();
671 crate::native_collection::register_native_collection_api(&mut engine);
672 crate::document::register_document_api(&mut engine);
673 #[cfg(feature = "charts")]
674 {
675 crate::chart::register_chart_data_api(&mut engine);
676 register_chart_validation_api(&mut engine);
677 }
678 engine.build_type::<crate::Span>();
679 register_ui_context_api(&mut engine);
680 register_date_api(&mut engine);
681 register_style_api(&mut engine);
682 register_motion_api(&mut engine);
683 register_text_area_api(&mut engine);
684 register_canvas_api(&mut engine);
685 crate::collection_projection::register_collection_projection_api(&mut engine);
686 let evaluation_generation = Rc::new(Cell::new(ScriptGeneration::default()));
687 let component_exports = ComponentExportCollector::new();
688 let (component_render, component_renderers) = register_component_runtime_apis(
689 &mut engine,
690 &component_exports,
691 &evaluation_generation,
692 );
693 let primitives = PrimitiveRegistry::new();
694 let native_handlers = crate::NativeHandlerRegistry::new();
695 let syntax_registry = crate::SyntaxRegistry::new();
696 let document_runtime = crate::DocumentRuntimeConfig::new();
697 #[cfg(feature = "charts")]
698 let chart_transforms = crate::ChartTransformRegistry::new();
699 #[cfg(feature = "charts")]
700 let chart_geo = crate::ChartGeoRegistry::new();
701 #[cfg(feature = "charts")]
702 let chart_series = crate::ChartSeriesRegistry::new();
703 #[cfg(feature = "charts")]
704 let chart_formatters = crate::ChartFormatterRegistry::new();
705 register_native_handler_api(&mut engine, &native_handlers);
706
707 register_node_apis(&mut engine);
708
709 configure_engine_limits(&mut engine);
710
711 let mut runtime = Self {
712 engine,
713 generation: ScriptGeneration::default(),
714 preparation_generation: None,
715 component_exports,
716 evaluation_generation,
717 primitives,
718 primitive_modules: BTreeMap::new(),
719 timings: RefCell::new(Vec::new()),
720 last_failed_timing: RefCell::new(None),
721 last_failed_component: RefCell::new(None),
722 operation_tracker,
723 operation_limit,
724 slow_threshold: Duration::from_millis(16),
725 component_render,
726 component_invocations: BTreeMap::new(),
727 component_effects: BTreeMap::new(),
728 component_timers: BTreeMap::new(),
729 component_signals: BTreeMap::new(),
730 component_element_refs: BTreeSet::new(),
731 pending_component_commits: BTreeMap::new(),
732 component_renderers,
733 native_handlers,
734 syntax_registry,
735 document_runtime,
736 #[cfg(feature = "charts")]
737 chart_transforms,
738 #[cfg(feature = "charts")]
739 chart_geo,
740 #[cfg(feature = "charts")]
741 chart_series,
742 #[cfg(feature = "charts")]
743 chart_formatters,
744 virtual_collections: BTreeMap::new(),
745 };
746 runtime.register_builtin_primitives();
747 runtime
748 }
749
750 fn register_builtin_primitives(&mut self) {
751 let column_measurements = ColumnMeasurementRegistry::default();
752 self.register_primitive(
753 column_resize_primitive_descriptor(),
754 ColumnResizePrimitiveHandler::new(column_measurements.clone()),
755 )
756 .expect("built-in column resize primitive descriptor is valid");
757 self.register_primitive(
758 intrinsic_text_measure_primitive_descriptor(),
759 IntrinsicTextMeasurePrimitiveHandler::new(column_measurements),
760 )
761 .expect("built-in intrinsic text measurement primitive descriptor is valid");
762 self.register_primitive(
763 split_resize_primitive_descriptor(),
764 SplitResizePrimitiveHandler,
765 )
766 .expect("built-in split resize primitive descriptor is valid");
767 self.register_primitive(
768 resizable_primitive_descriptor(),
769 ResizablePrimitiveHandler::default(),
770 )
771 .expect("built-in resizable primitive descriptor is valid");
772 self.register_primitive(draggable_primitive_descriptor(), DraggablePrimitiveHandler)
773 .expect("built-in draggable primitive descriptor is valid");
774 self.register_primitive(
775 drag_source_primitive_descriptor(),
776 DragSourcePrimitiveHandler::default(),
777 )
778 .expect("built-in drag source primitive descriptor is valid");
779 self.register_primitive(drop_zone_primitive_descriptor(), DropZonePrimitiveHandler)
780 .expect("built-in drop zone primitive descriptor is valid");
781 self.register_primitive(
782 sortable_primitive_descriptor(),
783 SortablePrimitiveHandler::default(),
784 )
785 .expect("built-in sortable primitive descriptor is valid");
786 self.register_primitive(
787 pan_zoom_primitive_descriptor(),
788 PanZoomPrimitiveHandler::default(),
789 )
790 .expect("built-in pan zoom primitive descriptor is valid");
791 self.register_primitive(
792 range_slider_primitive_descriptor(),
793 RangeSliderPrimitiveHandler::default(),
794 )
795 .expect("built-in range slider primitive descriptor is valid");
796 self.register_primitive(rotatable_primitive_descriptor(), RotatablePrimitiveHandler)
797 .expect("built-in rotatable primitive descriptor is valid");
798 self.register_primitive(
799 selection_area_primitive_descriptor(),
800 SelectionAreaPrimitiveHandler::default(),
801 )
802 .expect("built-in selection area primitive descriptor is valid");
803 self.register_primitive(
804 text_input_primitive_descriptor(),
805 TextInputPrimitiveHandler::default(),
806 )
807 .expect("built-in TextInput primitive descriptor is valid");
808 self.register_primitive(
809 text_area_primitive_descriptor(),
810 TextAreaPrimitiveHandler::default(),
811 )
812 .expect("built-in Textarea primitive descriptor is valid");
813 self.register_primitive(
814 range_input_primitive_descriptor(),
815 RangeInputPrimitiveHandler::default(),
816 )
817 .expect("built-in RangeInput primitive descriptor is valid");
818 self.register_primitive(
819 crate::code_viewer_primitive_descriptor(),
820 crate::CodeViewerPrimitiveHandler::new(
821 self.syntax_registry.clone(),
822 self.document_runtime.clone(),
823 ),
824 )
825 .expect("built-in CodeViewer primitive descriptor is valid");
826 self.register_primitive(
827 crate::diff_viewer_primitive_descriptor(),
828 crate::DiffViewerPrimitiveHandler::new(
829 self.syntax_registry.clone(),
830 self.document_runtime.clone(),
831 ),
832 )
833 .expect("built-in DiffViewer primitive descriptor is valid");
834 #[cfg(feature = "charts")]
835 self.register_primitive(
836 crate::chart_primitive_descriptor(),
837 crate::ChartPrimitiveHandler::new(
838 self.chart_transforms.clone(),
839 self.chart_geo.clone(),
840 self.chart_series.clone(),
841 self.chart_formatters.clone(),
842 ),
843 )
844 .expect("built-in Chart primitive descriptor is valid");
845 }
846
847 pub fn compile(&mut self, source: &str) -> Result<CompiledUi, RuntimeError> {
853 self.compile_named("ui/main.rhai", source)
854 }
855
856 pub fn compile_named(
862 &mut self,
863 source_name: &str,
864 source: &str,
865 ) -> Result<CompiledUi, RuntimeError> {
866 let generation = self.candidate_generation();
867 self.evaluation_generation.set(generation);
868 self.begin_execution_session();
869 let started = self.begin_timing();
870 let result = self.engine.compile(source);
871 self.record_timing(
872 ExecutionOperation::Compile,
873 source_name,
874 started,
875 result.is_ok(),
876 );
877 let mut ast = result.map_err(|error| RuntimeError::Compile(error.into()))?;
878 validate_assignment_targets(&ast)?;
879 ast.set_source(source_name);
880 Ok(CompiledUi { ast, generation })
881 }
882
883 pub fn compile_self_contained_named(
889 &mut self,
890 source_name: &str,
891 source: &str,
892 ) -> Result<CompiledUi, RuntimeError> {
893 let generation = self.candidate_generation();
894 self.evaluation_generation.set(generation);
895 self.begin_execution_session();
896 let started = self.begin_timing();
897 let result = self
898 .engine
899 .compile_into_self_contained(&Scope::new(), source);
900 self.record_timing(
901 ExecutionOperation::Compile,
902 source_name,
903 started,
904 result.is_ok(),
905 );
906 let mut ast = result.map_err(RuntimeError::Compile)?;
907 validate_literal_imports(&ast)?;
908 validate_assignment_targets(&ast)?;
909 ast.set_source(source_name);
910 Ok(CompiledUi { ast, generation })
911 }
912
913 pub fn set_module_resolver(&mut self, resolver: impl ModuleResolver + 'static) {
914 self.engine.set_module_resolver(resolver);
915 }
916
917 pub fn with_program_preparation<T, E>(
925 &mut self,
926 operation: impl FnOnce(&mut Self) -> Result<T, E>,
927 ) -> Result<T, E> {
928 if self.preparation_generation.is_some() {
929 return operation(self);
930 }
931 let generation = Self::allocate_generation();
932 self.preparation_generation = Some(generation);
933 let result = operation(self);
934 self.preparation_generation = None;
935 result
936 }
937
938 fn begin_component_render(
939 &self,
940 context: UiContext,
941 generation: ScriptGeneration,
942 root_effects: Option<BTreeSet<String>>,
943 reuse_plan: Option<ComponentReusePlan>,
944 ) -> Result<(), RuntimeError> {
945 let root = context.component_path().clone();
946 context.reset_non_reusable_render_reads();
947 let (state_snapshot, environment, event_handlers) = {
948 let runtime = context.runtime().try_borrow().map_err(|_| {
949 RuntimeError::ComponentRuntime("UI state is already borrowed".to_owned())
950 })?;
951 (
952 runtime.component_state.clone(),
953 ComponentRenderEnvironment {
954 theme_generation: runtime.theme.as_ref().map(crate::ThemeManager::generation),
955 component_style_generation: runtime.component_style_generation(),
956 locale_generation: runtime
957 .locale
958 .as_ref()
959 .map(crate::LocaleManager::generation),
960 calendar_day: runtime.calendar_clock.today(),
961 },
962 reuse_plan
963 .as_ref()
964 .map(|_| runtime.component_event_handlers_in_scope(&root)),
965 )
966 };
967 let mut transaction = context
968 .runtime()
969 .try_borrow()
970 .map_err(|_| RuntimeError::ComponentRuntime("UI state is already borrowed".to_owned()))?
971 .component_state
972 .begin_render_scope(root.clone());
973 let _ = transaction.retain_existing(&root);
974 let reuse = reuse_plan.map(|plan| ComponentReuseSnapshot {
975 subtrees: plan.subtrees,
976 previous_root: plan.previous_root,
977 dirty: plan.dirty,
978 invocations: self
979 .component_invocations
980 .iter()
981 .filter(|(path, _)| path.is_within(&root))
982 .map(|(path, recipe)| (path.clone(), recipe.clone()))
983 .collect(),
984 event_handlers: event_handlers.unwrap_or_default(),
985 effects: self
986 .component_effects
987 .iter()
988 .filter(|(id, _)| id.component().is_within(&root))
989 .map(|(id, descriptor)| (id.clone(), descriptor.clone()))
990 .collect(),
991 timers: self
992 .component_timers
993 .iter()
994 .filter(|(id, _)| id.component().is_within(&root))
995 .map(|(id, descriptor)| (id.clone(), descriptor.clone()))
996 .collect(),
997 signals: self
998 .component_signals
999 .iter()
1000 .filter(|(id, _)| id.component().is_within(&root))
1001 .map(|(id, descriptor)| (id.clone(), descriptor.clone()))
1002 .collect(),
1003 element_refs: self
1004 .component_element_refs
1005 .iter()
1006 .filter(|id| id.component().is_within(&root))
1007 .cloned()
1008 .collect(),
1009 virtual_collections: self
1010 .virtual_collections
1011 .iter()
1012 .filter(|(id, _)| id.component.is_within(&root))
1013 .map(|(id, recipe)| (id.clone(), recipe.clone()))
1014 .collect(),
1015 });
1016 let mut active = self.component_render.try_borrow_mut().map_err(|_| {
1017 RuntimeError::ComponentRuntime("component render stack is already borrowed".to_owned())
1018 })?;
1019 *active = Some(ActiveComponentRender {
1020 root_context: context.clone(),
1021 stack: vec![root],
1022 contexts: vec![context],
1023 seen: BTreeSet::new(),
1024 positions: BTreeMap::new(),
1025 transaction,
1026 generation,
1027 state_snapshot,
1028 event_handlers: BTreeMap::new(),
1029 invocations: BTreeMap::new(),
1030 effect_keys: vec![root_effects],
1031 effects: BTreeMap::new(),
1032 timers: BTreeMap::new(),
1033 signals: BTreeMap::new(),
1034 element_refs: BTreeSet::new(),
1035 virtual_collections: BTreeMap::new(),
1036 environment,
1037 reuse,
1038 reused: Vec::new(),
1039 failed_component: None,
1040 });
1041 Ok(())
1042 }
1043
1044 fn finish_component_render(&mut self, commit: bool) -> Result<(), RuntimeError> {
1045 let active = self
1046 .component_render
1047 .try_borrow_mut()
1048 .map_err(|_| {
1049 RuntimeError::ComponentRuntime(
1050 "component render stack is already borrowed".to_owned(),
1051 )
1052 })?
1053 .take();
1054 let Some(active) = active else {
1055 return Ok(());
1056 };
1057 if commit {
1058 self.timings
1059 .borrow_mut()
1060 .extend(
1061 active
1062 .reused
1063 .iter()
1064 .map(|(source, components)| ExecutionTiming {
1065 operation: ExecutionOperation::ComponentReuse(*components),
1066 source: source.clone(),
1067 duration: Duration::ZERO,
1068 operations: 0,
1069 operation_limit: self.operation_limit(),
1070 round_operations: self.operation_total(),
1071 operation_semantics: OPERATION_SEMANTICS_VERSION,
1072 slow: false,
1073 succeeded: true,
1074 }),
1075 );
1076 let root = active.root_context.component_path().clone();
1077 let previous = active.state_snapshot.paths();
1078 self.pending_component_commits.insert(
1079 root.clone(),
1080 PendingComponentCommit {
1081 root: root.clone(),
1082 previous,
1083 retained_root: (!active.invocations.contains_key(&root)
1084 && active.transaction.retains_root())
1085 .then_some(root.clone()),
1086 transaction: active.transaction,
1087 event_handlers: active.event_handlers,
1088 },
1089 );
1090 self.component_invocations
1091 .retain(|path, _| !path.is_within(&root));
1092 self.component_invocations.extend(active.invocations);
1093 self.component_effects
1094 .retain(|id, _| !id.component().is_within(&root));
1095 self.component_effects.extend(active.effects);
1096 self.component_timers
1097 .retain(|id, _| !id.component().is_within(&root));
1098 self.component_timers.extend(active.timers);
1099 self.component_signals
1100 .retain(|id, _| !id.component().is_within(&root));
1101 self.component_signals.extend(active.signals);
1102 self.component_element_refs
1103 .retain(|id| !id.component().is_within(&root));
1104 self.component_element_refs.extend(active.element_refs);
1105 self.virtual_collections
1106 .retain(|id, _| !id.component.is_within(&root));
1107 self.virtual_collections.extend(active.virtual_collections);
1108 } else {
1109 *self.last_failed_component.borrow_mut() = active
1110 .failed_component
1111 .or_else(|| active.stack.last().cloned());
1112 active
1113 .root_context
1114 .runtime()
1115 .try_borrow_mut()
1116 .map_err(|_| {
1117 RuntimeError::ComponentRuntime("UI state is already borrowed".to_owned())
1118 })?
1119 .component_state = active.state_snapshot;
1120 }
1121 Ok(())
1122 }
1123
1124 pub fn render(&mut self, compiled: &CompiledUi) -> Result<UiNode, RuntimeError> {
1131 self.evaluation_generation.set(compiled.generation);
1132 self.begin_execution_session();
1133 let checkpoint = self.execution_checkpoint();
1134 let root_path = ComponentInstancePath::root("App", "root");
1135 let runtime = Rc::new(RefCell::new(UiRuntimeState::new()));
1136 let context = UiContext::new(
1137 Rc::clone(&runtime),
1138 root_path.clone(),
1139 None,
1140 ExecutionPhase::Render,
1141 BTreeMap::new(),
1142 )
1143 .with_generation(compiled.generation);
1144 self.begin_component_render(context, compiled.generation, None, None)?;
1145 let started = self.begin_timing();
1146 let result = AstInterpreter::call_fn::<UiNode, _>(
1147 &self.engine,
1148 &compiled.ast,
1149 &mut Scope::new(),
1150 "view",
1151 (),
1152 );
1153 self.record_timing(
1154 ExecutionOperation::Render,
1155 compiled.ast.source().unwrap_or("<script>"),
1156 started,
1157 result.is_ok(),
1158 );
1159 self.finish_component_render(result.is_ok())?;
1160 let mut root = result.map_err(RuntimeError::Evaluate)?;
1161 if !self.component_effects_in_scope(&root_path).is_empty()
1162 || !self.component_timers_in_scope(&root_path).is_empty()
1163 || !self.component_signals_in_scope(&root_path).is_empty()
1164 || !self.component_element_refs_in_scope(&root_path).is_empty()
1165 || !self.virtual_collection_ids_in_scope(&root_path).is_empty()
1166 {
1167 self.restore_execution_checkpoint(checkpoint);
1168 return Err(RuntimeError::ComponentRuntime(
1169 "effectful, timer/signal/ref-owning, or virtual components require ScriptLifecycle"
1170 .to_owned(),
1171 ));
1172 }
1173 if let Err(error) = self.commit_component_renders(&mut runtime.borrow_mut()) {
1174 self.restore_execution_checkpoint(checkpoint);
1175 return Err(error);
1176 }
1177 root.bind_generation(compiled.generation);
1178 self.generation = compiled.generation;
1179 Ok(root)
1180 }
1181
1182 pub fn render_with_context(
1189 &mut self,
1190 compiled: &CompiledUi,
1191 context: UiContext,
1192 ) -> Result<UiNode, RuntimeError> {
1193 let runtime = Rc::clone(context.runtime());
1194 let root = context.component_path().clone();
1195 let snapshot = runtime
1196 .try_borrow_mut()
1197 .map_err(|_| RuntimeError::ComponentRuntime("UI state is already borrowed".to_owned()))?
1198 .begin_transaction()
1199 .map_err(|error| RuntimeError::ComponentRuntime(error.to_string()))?;
1200 let checkpoint = self.execution_checkpoint();
1201 let result = self.render_with_context_staged(compiled, context);
1202 let result = result.and_then(|node| {
1203 if !self.component_effects_in_scope(&root).is_empty()
1204 || !self.component_timers_in_scope(&root).is_empty()
1205 || !self.component_signals_in_scope(&root).is_empty()
1206 || !self.component_element_refs_in_scope(&root).is_empty()
1207 || !self.virtual_collection_ids_in_scope(&root).is_empty()
1208 {
1209 return Err(RuntimeError::ComponentRuntime(
1210 "effectful, timer/signal/ref-owning, or virtual components require ScriptLifecycle"
1211 .to_owned(),
1212 ));
1213 }
1214 self.commit_component_renders(&mut runtime.borrow_mut())?;
1215 Ok(node)
1216 });
1217 match result {
1218 Ok(node) => {
1219 runtime
1220 .try_borrow_mut()
1221 .map_err(|_| {
1222 RuntimeError::ComponentRuntime("UI state is already borrowed".to_owned())
1223 })?
1224 .commit_transaction()
1225 .map_err(|error| RuntimeError::ComponentRuntime(error.to_string()))?;
1226 Ok(node)
1227 }
1228 Err(error) => {
1229 runtime
1230 .try_borrow_mut()
1231 .map_err(|_| {
1232 RuntimeError::ComponentRuntime("UI state is already borrowed".to_owned())
1233 })?
1234 .restore(snapshot)
1235 .map_err(|restore| RuntimeError::ComponentRuntime(restore.to_string()))?;
1236 self.restore_execution_checkpoint(checkpoint);
1237 Err(error)
1238 }
1239 }
1240 }
1241
1242 pub(crate) fn render_with_context_staged(
1243 &mut self,
1244 compiled: &CompiledUi,
1245 context: UiContext,
1246 ) -> Result<UiNode, RuntimeError> {
1247 self.render_with_context_staged_impl(compiled, context, None)
1248 }
1249
1250 pub(crate) fn render_with_context_staged_reusing(
1251 &mut self,
1252 compiled: &CompiledUi,
1253 context: UiContext,
1254 previous_root: Rc<UiNode>,
1255 dirty: &BTreeSet<ComponentInstancePath>,
1256 ) -> Result<UiNode, RuntimeError> {
1257 self.render_with_context_staged_impl(
1258 compiled,
1259 context,
1260 Some(ComponentReusePlan::new(previous_root, dirty.clone())),
1261 )
1262 }
1263
1264 fn render_with_context_staged_impl(
1265 &mut self,
1266 compiled: &CompiledUi,
1267 context: UiContext,
1268 reuse: Option<ComponentReusePlan>,
1269 ) -> Result<UiNode, RuntimeError> {
1270 self.evaluation_generation.set(compiled.generation);
1271 self.begin_execution_session();
1272 self.begin_component_render(context.clone(), compiled.generation, None, reuse)?;
1273 let started = self.begin_timing();
1274 let result = AstInterpreter::call_fn::<UiNode, _>(
1275 &self.engine,
1276 &compiled.ast,
1277 &mut Scope::new(),
1278 "view",
1279 (context,),
1280 );
1281 self.record_timing(
1282 ExecutionOperation::Render,
1283 compiled.ast.source().unwrap_or("<script>"),
1284 started,
1285 result.is_ok(),
1286 );
1287 self.finish_component_render(result.is_ok())?;
1288 let mut root = result.map_err(RuntimeError::Evaluate)?;
1289 root.bind_generation(compiled.generation);
1290 self.generation = compiled.generation;
1291 Ok(root)
1292 }
1293
1294 pub(crate) fn rerender_component(
1295 &mut self,
1296 component: &ComponentInstancePath,
1297 previous_root: &UiNode,
1298 dirty: &BTreeSet<ComponentInstancePath>,
1299 ) -> Result<UiNode, RuntimeError> {
1300 let mut recipe = self
1301 .component_invocations
1302 .get(component)
1303 .cloned()
1304 .ok_or_else(|| RuntimeError::MissingComponentInvocation(component.clone()))?;
1305 if recipe.generation != self.generation {
1306 return Err(RuntimeError::StaleCallback {
1307 name: recipe.render.fn_name().to_owned(),
1308 callback_generation: recipe.generation,
1309 current_generation: self.generation,
1310 });
1311 }
1312 recipe
1313 .component_context
1314 .runtime()
1315 .try_borrow_mut()
1316 .map_err(|_| RuntimeError::ComponentRuntime("UI state is already borrowed".to_owned()))?
1317 .reset_component_readers(component);
1318 for value in recipe.script_props.values_mut() {
1319 hydrate_replayed_node_value(value);
1320 }
1321 let reuse = ComponentReusePlan::new(Rc::new(previous_root.clone()), dirty.clone());
1322 self.begin_component_render(
1323 recipe.component_context.clone(),
1324 recipe.generation,
1325 Some(recipe.declared_effects.clone()),
1326 Some(reuse),
1327 )?;
1328 register_component_invocation(&self.component_render, recipe.clone())
1329 .map_err(RuntimeError::Evaluate)?;
1330
1331 self.align_execution_session_to(recipe.context.operation_base());
1332 let started = self.begin_timing();
1333 let result = (|| {
1334 let mut node = recipe
1335 .context
1336 .call::<UiNode>(
1337 self.engine(),
1338 &recipe.render,
1339 (
1340 recipe.component_context.clone(),
1341 recipe.script_props.clone(),
1342 ),
1343 )
1344 .map_err(RuntimeError::Evaluate)?;
1345 register_component_event_callbacks(
1346 &self.component_render,
1347 &recipe.path,
1348 &recipe.caller_context,
1349 recipe.event_callbacks.clone(),
1350 &recipe.context,
1351 )
1352 .map_err(RuntimeError::Evaluate)?;
1353 node = node.with_owned_part_styles(recipe.part_styles.clone());
1354 node =
1355 node.with_component_root_snapshot(recipe.path.clone(), recipe.snapshot.reference());
1356 node.bind_component_scope(
1357 &recipe.path,
1358 recipe.component_context.component_incarnation(),
1359 &recipe.component_events,
1360 Some(&recipe.context),
1361 );
1362 node.bind_generation(recipe.generation);
1363 Ok(node)
1364 })();
1365 if result.is_err() {
1366 record_component_render_failure(&self.component_render, &recipe.path);
1367 }
1368 {
1369 let mut active = self.component_render.try_borrow_mut().map_err(|_| {
1370 RuntimeError::ComponentRuntime(
1371 "component render stack is already borrowed".to_owned(),
1372 )
1373 })?;
1374 if let Some(active) = active.as_mut() {
1375 let environment = active.environment;
1376 if let Some(retained) = active.invocations.get_mut(&recipe.path) {
1377 retained.environment = environment;
1378 retained.reusable = recipe.component_context.component_render_is_reusable();
1379 if let Ok(node) = &result {
1380 retained.snapshot.update_if_active(node);
1381 }
1382 }
1383 }
1384 }
1385 self.record_timing(
1386 ExecutionOperation::Render,
1387 recipe.component.as_str(),
1388 started,
1389 result.is_ok(),
1390 );
1391 self.finish_component_render(result.is_ok())?;
1392 result
1393 }
1394
1395 pub(crate) fn execution_checkpoint(&self) -> RuntimeEngineCheckpoint {
1396 RuntimeEngineCheckpoint {
1397 generation: self.generation,
1398 evaluation_generation: self.evaluation_generation.get(),
1399 component_invocations: self.component_invocations.clone(),
1400 component_effects: self.component_effects.clone(),
1401 component_timers: self.component_timers.clone(),
1402 component_signals: self.component_signals.clone(),
1403 component_element_refs: self.component_element_refs.clone(),
1404 pending_component_commits: self.pending_component_commits.clone(),
1405 virtual_collections: self.virtual_collections.clone(),
1406 component_snapshots: self
1407 .component_invocations
1408 .values()
1409 .map(|recipe| (recipe.snapshot.clone(), recipe.snapshot.current()))
1410 .collect(),
1411 }
1412 }
1413
1414 pub(crate) fn restore_execution_checkpoint(&mut self, checkpoint: RuntimeEngineCheckpoint) {
1415 for (snapshot, value) in &checkpoint.component_snapshots {
1416 snapshot.restore(value.clone());
1417 }
1418 self.generation = checkpoint.generation;
1419 self.evaluation_generation
1420 .set(checkpoint.evaluation_generation);
1421 self.component_invocations = checkpoint.component_invocations;
1422 self.component_effects = checkpoint.component_effects;
1423 self.component_timers = checkpoint.component_timers;
1424 self.component_signals = checkpoint.component_signals;
1425 self.component_element_refs = checkpoint.component_element_refs;
1426 self.pending_component_commits = checkpoint.pending_component_commits;
1427 self.virtual_collections = checkpoint.virtual_collections;
1428 }
1429
1430 pub(crate) fn commit_component_renders(
1431 &mut self,
1432 runtime: &mut UiRuntimeState,
1433 ) -> Result<(), RuntimeError> {
1434 let commits = std::mem::take(&mut self.pending_component_commits);
1435 if commits.is_empty() {
1436 return Ok(());
1437 }
1438 let mut active = self
1441 .component_invocations
1442 .keys()
1443 .cloned()
1444 .collect::<BTreeSet<_>>();
1445 active.extend(
1446 self.virtual_collections
1447 .keys()
1448 .map(|id| id.component.child("VirtualCollection", id.key.clone())),
1449 );
1450 active.extend(
1451 commits
1452 .values()
1453 .filter_map(|commit| commit.retained_root.clone()),
1454 );
1455 let roots = crate::state::topmost_paths(&commits.keys().cloned().collect());
1456 let previous = commits
1457 .values()
1458 .flat_map(|commit| commit.previous.iter().cloned())
1459 .collect();
1460 let mut transactions = Vec::with_capacity(commits.len());
1461 for (_, commit) in commits {
1462 runtime.replace_component_event_handlers(&commit.root, commit.event_handlers);
1463 transactions.push(commit.transaction);
1464 }
1465 runtime
1466 .component_state
1467 .commit_render_batch(transactions, &active);
1468 for root in roots {
1469 runtime
1470 .reconcile_component_lifetimes(&root, &active, &previous)
1471 .map_err(|error| RuntimeError::ComponentRuntime(error.to_string()))?;
1472 }
1473 Ok(())
1474 }
1475
1476 pub(crate) fn component_effects_in_scope(
1477 &self,
1478 root: &ComponentInstancePath,
1479 ) -> BTreeMap<crate::EffectId, crate::EffectDescriptor> {
1480 self.component_effects
1481 .iter()
1482 .filter(|(id, _)| id.component().is_within(root))
1483 .map(|(id, descriptor)| (id.clone(), descriptor.clone()))
1484 .collect()
1485 }
1486
1487 pub(crate) fn component_signals_in_scope(
1488 &self,
1489 root: &ComponentInstancePath,
1490 ) -> BTreeMap<crate::SignalId, crate::signal::SignalDescriptor> {
1491 self.component_signals
1492 .iter()
1493 .filter(|(id, _)| id.component().is_within(root))
1494 .map(|(id, descriptor)| (id.clone(), descriptor.clone()))
1495 .collect()
1496 }
1497
1498 pub(crate) fn component_timers_in_scope(
1499 &self,
1500 root: &ComponentInstancePath,
1501 ) -> BTreeMap<crate::TimerId, crate::TimerDescriptor> {
1502 self.component_timers
1503 .iter()
1504 .filter(|(id, _)| id.component().is_within(root))
1505 .map(|(id, descriptor)| (id.clone(), descriptor.clone()))
1506 .collect()
1507 }
1508
1509 pub(crate) fn component_element_refs_in_scope(
1510 &self,
1511 root: &ComponentInstancePath,
1512 ) -> BTreeSet<crate::ElementRefId> {
1513 self.component_element_refs
1514 .iter()
1515 .filter(|id| id.component().is_within(root))
1516 .cloned()
1517 .collect()
1518 }
1519
1520 pub(crate) fn virtual_collection_ids_in_scope(
1521 &self,
1522 root: &ComponentInstancePath,
1523 ) -> BTreeSet<crate::VirtualCollectionId> {
1524 self.virtual_collections
1525 .keys()
1526 .filter(|id| id.component.is_within(root))
1527 .cloned()
1528 .collect()
1529 }
1530
1531 pub(crate) fn realize_virtual_collection(
1532 &mut self,
1533 id: &crate::VirtualCollectionId,
1534 indices: &BTreeSet<usize>,
1535 previous: &BTreeMap<usize, UiNode>,
1536 ) -> Result<BTreeMap<usize, UiNode>, RuntimeError> {
1537 let recipe = self.virtual_collections.get(id).cloned().ok_or_else(|| {
1538 RuntimeError::ComponentRuntime(format!(
1539 "virtual collection `{}` is not retained in the active generation",
1540 id.key
1541 ))
1542 })?;
1543 if recipe.generation != self.generation || recipe.id != *id {
1544 return Err(RuntimeError::StaleCallback {
1545 name: recipe.renderer.fn_name().to_owned(),
1546 callback_generation: recipe.generation,
1547 current_generation: self.generation,
1548 });
1549 }
1550 let scope = id.component.child("VirtualCollection", id.key.clone());
1551 let context = recipe
1552 .context
1553 .for_structural_scope(scope, BTreeMap::new())
1554 .with_generation(recipe.generation);
1555 let retained = previous
1556 .iter()
1557 .filter(|(index, _)| indices.contains(index))
1558 .map(|(index, node)| (*index, node.clone()))
1559 .collect::<BTreeMap<_, _>>();
1560 let previous_root = Rc::new(UiNode::box_node(retained.values().cloned().collect()));
1561 prune_virtual_read_contributions(&context, id, &recipe.data, indices, &previous_root)?;
1562 self.begin_component_render(
1563 context.clone(),
1564 recipe.generation,
1565 None,
1566 Some(ComponentReusePlan::new(previous_root, BTreeSet::new())),
1567 )?;
1568 retain_virtual_component_manifest(&self.component_render)?;
1569 let result = (|| {
1570 let mut realized = retained;
1571 for index in indices
1572 .iter()
1573 .copied()
1574 .filter(|index| *index < recipe.data.len() && !previous.contains_key(index))
1575 {
1576 let item = recipe
1577 .data
1578 .item(index)
1579 .map_err(|error| RuntimeError::ComponentRuntime(error.to_string()))?
1580 .ok_or_else(|| {
1581 RuntimeError::ComponentRuntime(format!(
1582 "virtual collection item {index} disappeared"
1583 ))
1584 })?;
1585 let (key, mut payload) =
1586 collection_payload(&item, index).map_err(RuntimeError::Evaluate)?;
1587 add_collection_neighbors(&recipe.data, index, &mut payload);
1588 let invocation = &recipe.renderer_context;
1589 self.align_execution_session_to(invocation.operation_base());
1590 let started = self.begin_timing();
1591 let item_context = virtual_item_context(&context, id, &key);
1592 enter_virtual_collection_scope(&self.component_render, item_context.clone())
1593 .map_err(RuntimeError::Evaluate)?;
1594 let item = invocation.call::<UiNode>(
1595 self.engine(),
1596 &recipe.renderer,
1597 (item_context, Dynamic::from_map(payload)),
1598 );
1599 leave_component_render(&self.component_render).map_err(RuntimeError::Evaluate)?;
1600 self.record_timing(
1601 ExecutionOperation::VirtualCollection(id.key.clone()),
1602 id.key.as_str(),
1603 started,
1604 item.is_ok(),
1605 );
1606 let mut node = item.map_err(RuntimeError::Evaluate)?.with_key(key);
1607 node.bind_generation(recipe.generation);
1608 node.bind_component_scope(
1609 recipe.context.callback_component_path(),
1610 recipe.context.callback_component_incarnation(),
1611 recipe.context.callback_event_schemas(),
1612 Some(&recipe.renderer_context),
1613 );
1614 realized.insert(index, node);
1615 }
1616 Ok(realized)
1617 })();
1618 if result.is_err() {
1619 record_component_render_failure(&self.component_render, context.component_path());
1620 }
1621 self.finish_component_render(result.is_ok())?;
1622 result
1623 }
1624
1625 pub(crate) fn update_virtual_collection_snapshot(
1626 &mut self,
1627 id: &crate::VirtualCollectionId,
1628 items: &BTreeMap<usize, UiNode>,
1629 ) {
1630 let mut path = Some(id.component.clone());
1634 while let Some(current_path) = path {
1635 if let Some(owner) = self.component_invocations.get_mut(¤t_path)
1636 && let Some(current) = owner.snapshot.current()
1637 && current.virtual_collection_spec(id).is_some()
1638 {
1639 let mut rendered = current.as_ref().clone();
1640 rendered.replace_virtual_collection_items(id, items.clone());
1641 owner.snapshot.replace_if_active(Rc::new(rendered));
1642 }
1643 path = current_path.parent();
1644 }
1645 }
1646
1647 pub fn call_optional_lifecycle(
1653 &self,
1654 compiled: &CompiledUi,
1655 function: &str,
1656 context: UiContext,
1657 ) -> Result<bool, RuntimeError> {
1658 if !compiled.has_function(function, 1) {
1659 return Ok(false);
1660 }
1661 self.evaluation_generation.set(compiled.generation);
1662 self.begin_execution_session();
1663 let started = self.begin_timing();
1664 let result = AstInterpreter::call_fn::<Dynamic, _>(
1665 &self.engine,
1666 &compiled.ast,
1667 &mut Scope::new(),
1668 function,
1669 (context,),
1670 );
1671 self.record_timing(
1672 ExecutionOperation::Lifecycle(function.to_owned()),
1673 compiled.ast.source().unwrap_or("<script>"),
1674 started,
1675 result.is_ok(),
1676 );
1677 let _ = result.map_err(RuntimeError::Evaluate)?;
1678 Ok(true)
1679 }
1680
1681 pub(crate) fn call_optional_lifecycle_with_value(
1682 &self,
1683 compiled: &CompiledUi,
1684 function: &str,
1685 context: UiContext,
1686 value: UiValue,
1687 ) -> Result<bool, RuntimeError> {
1688 if !compiled.has_function(function, 2) {
1689 return Ok(false);
1690 }
1691 self.evaluation_generation.set(compiled.generation);
1692 self.begin_execution_session();
1693 let started = self.begin_timing();
1694 let result = AstInterpreter::call_fn::<Dynamic, _>(
1695 &self.engine,
1696 &compiled.ast,
1697 &mut Scope::new(),
1698 function,
1699 (context, value.into_dynamic()),
1700 );
1701 self.record_timing(
1702 ExecutionOperation::Lifecycle(function.to_owned()),
1703 compiled.ast.source().unwrap_or("<script>"),
1704 started,
1705 result.is_ok(),
1706 );
1707 let _ = result.map_err(RuntimeError::Evaluate)?;
1708 Ok(true)
1709 }
1710
1711 pub fn root_state_schema(
1718 &self,
1719 compiled: &CompiledUi,
1720 ) -> Result<ComponentStateSchema, RuntimeError> {
1721 if !compiled.has_function("state_schema", 0) {
1722 return Ok(ComponentStateSchema::default());
1723 }
1724 self.begin_execution_session();
1725 let started = self.begin_timing();
1726 let result = (|| {
1727 let raw: Dynamic = self
1728 .engine
1729 .call_fn(&mut Scope::new(), &compiled.ast, "state_schema", ())
1730 .map_err(RuntimeError::Evaluate)?;
1731 validate_state_default_encoding(&raw, "state_schema")
1732 .map_err(RuntimeError::Evaluate)?;
1733 rhai::serde::from_dynamic(&raw).map_err(RuntimeError::Evaluate)
1734 })();
1735 self.record_timing(
1736 ExecutionOperation::Lifecycle("state_schema".to_owned()),
1737 compiled.ast.source().unwrap_or("<script>"),
1738 started,
1739 result.is_ok(),
1740 );
1741 result
1742 }
1743
1744 pub fn callback(
1751 &self,
1752 compiled: &CompiledUi,
1753 function_name: &str,
1754 ) -> Result<ScriptCallback, RuntimeError> {
1755 let function =
1756 FnPtr::new(function_name).map_err(|source| RuntimeError::CallbackDefinition {
1757 name: function_name.to_owned(),
1758 source,
1759 })?;
1760 ScriptCallback::try_from_fn_ptr(function, compiled.generation).map_err(|source| {
1761 RuntimeError::RetainedCallback {
1762 name: function_name.to_owned(),
1763 source,
1764 }
1765 })
1766 }
1767
1768 pub fn invoke_callback(
1775 &self,
1776 compiled: &CompiledUi,
1777 callback: &ScriptCallback,
1778 args: impl FuncArgs,
1779 ) -> Result<Dynamic, RuntimeError> {
1780 if !self.is_current(callback.generation) || callback.generation != compiled.generation {
1781 return Err(RuntimeError::StaleCallback {
1782 name: callback.name().to_owned(),
1783 callback_generation: callback.generation,
1784 current_generation: self.generation,
1785 });
1786 }
1787
1788 self.invoke_callback_for_generation(compiled, callback, args)
1789 }
1790
1791 pub(crate) fn invoke_callback_for_generation(
1792 &self,
1793 compiled: &CompiledUi,
1794 callback: &ScriptCallback,
1795 args: impl FuncArgs,
1796 ) -> Result<Dynamic, RuntimeError> {
1797 if callback.generation != compiled.generation {
1798 return Err(RuntimeError::StaleCallback {
1799 name: callback.name().to_owned(),
1800 callback_generation: callback.generation,
1801 current_generation: compiled.generation,
1802 });
1803 }
1804 self.evaluation_generation.set(compiled.generation);
1805 let operation_base = callback.native_context.as_ref().map_or(
1806 0,
1807 crate::invocation::ScriptInvocationContext::operation_base,
1808 );
1809 self.begin_execution_session_from(operation_base);
1810 let started = self.begin_timing();
1811 #[allow(deprecated)]
1812 let result = if let Some(context) = callback.native_context.as_ref() {
1813 context.call(self.engine(), &callback.function, args)
1814 } else {
1815 callback.function.call(self.engine(), &compiled.ast, args)
1816 };
1817 self.record_timing(
1818 ExecutionOperation::Callback(callback.name().to_owned()),
1819 compiled.ast.source().unwrap_or("<script>"),
1820 started,
1821 result.is_ok(),
1822 );
1823 result.map_err(RuntimeError::Evaluate)
1824 }
1825
1826 #[must_use]
1827 pub fn is_current(&self, generation: ScriptGeneration) -> bool {
1828 generation == self.generation
1829 }
1830
1831 #[must_use]
1832 pub fn engine(&self) -> &Engine {
1833 &self.engine
1834 }
1835
1836 #[must_use]
1841 pub fn definition_source(&self) -> String {
1842 crate::script_docs::ScriptApi::from_engine(&self.engine).map_or_else(
1845 |_| {
1846 self.engine
1847 .definitions()
1848 .include_standard_packages(false)
1849 .single_file()
1850 },
1851 |api| api.definitions(),
1852 )
1853 }
1854
1855 pub fn lint_known_calls(
1867 &mut self,
1868 entry_name: &str,
1869 entry_source: &str,
1870 modules: &BTreeMap<ModuleId, String>,
1871 ) -> Result<Vec<crate::KnownCallDiagnostic>, crate::KnownCallLintError> {
1872 crate::script_lint::lint_known_calls(
1873 &mut self.engine,
1874 entry_name,
1875 entry_source,
1876 modules,
1877 &BTreeSet::new(),
1878 )
1879 .map(|(diagnostics, _)| diagnostics)
1880 }
1881
1882 pub fn lint_known_calls_with_host(
1891 &mut self,
1892 entry_name: &str,
1893 entry_source: &str,
1894 modules: &BTreeMap<ModuleId, String>,
1895 host: &BTreeSet<crate::HostFunction>,
1896 ) -> Result<
1897 (
1898 Vec<crate::KnownCallDiagnostic>,
1899 BTreeSet<crate::HostFunction>,
1900 ),
1901 crate::KnownCallLintError,
1902 > {
1903 crate::script_lint::lint_known_calls(
1904 &mut self.engine,
1905 entry_name,
1906 entry_source,
1907 modules,
1908 host,
1909 )
1910 }
1911
1912 pub fn lint_shadowed_builtins(
1921 &mut self,
1922 entry_name: &str,
1923 entry_source: &str,
1924 ) -> Result<Vec<crate::ShadowedBuiltin>, crate::KnownCallLintError> {
1925 crate::script_lint::lint_shadowed_builtins(&mut self.engine, entry_name, entry_source)
1926 }
1927
1928 pub fn component_exports(&self) -> Result<ComponentRegistry, ComponentExportError> {
1935 self.component_exports.snapshot()
1936 }
1937
1938 pub fn component_invocations(
1939 &self,
1940 ) -> impl ExactSizeIterator<Item = &ComponentInvocationRecipe> {
1941 self.component_invocations.values()
1942 }
1943
1944 pub(crate) fn component_renderer_snapshot(
1945 &self,
1946 ) -> Result<BTreeMap<ModuleId, RegisteredComponentRender>, ComponentExportError> {
1947 self.component_renderers
1948 .try_borrow()
1949 .map(|renderers| renderers.clone())
1950 .map_err(|_| ComponentExportError::Borrowed)
1951 }
1952
1953 pub(crate) fn restore_component_renderers(
1954 &self,
1955 renderers: BTreeMap<ModuleId, RegisteredComponentRender>,
1956 ) -> Result<(), ComponentExportError> {
1957 *self
1958 .component_renderers
1959 .try_borrow_mut()
1960 .map_err(|_| ComponentExportError::Borrowed)? = renderers;
1961 Ok(())
1962 }
1963
1964 pub fn clear_component_exports(&self) -> Result<(), ComponentExportError> {
1970 self.component_renderers
1971 .try_borrow_mut()
1972 .map_err(|_| ComponentExportError::Borrowed)?
1973 .clear();
1974 self.component_exports.clear()
1975 }
1976
1977 pub fn restore_component_exports(
1983 &self,
1984 registry: ComponentRegistry,
1985 ) -> Result<(), ComponentExportError> {
1986 self.component_exports.replace(registry)
1987 }
1988
1989 pub fn register_primitive(
1995 &mut self,
1996 descriptor: PrimitiveDescriptor,
1997 handler: impl PrimitiveHandler + 'static,
1998 ) -> Result<(), PrimitiveError> {
1999 self.primitives.register(descriptor.clone(), handler)?;
2000
2001 let namespace = descriptor.id.namespace().to_owned();
2002 let export = descriptor.export.clone();
2003 let id = descriptor.id;
2004 let primitives = self.primitives.clone();
2005 let generation = Rc::clone(&self.evaluation_generation);
2006 let module = self.primitive_modules.entry(namespace.clone()).or_default();
2007 FuncRegistration::new(export).set_into_module(
2008 module,
2009 move |mut props: Map| -> Result<UiNode, Box<EvalAltResult>> {
2010 let key = props
2011 .remove("key")
2012 .map(|value| {
2013 if value.is::<ImmutableString>() {
2014 Ok(value.cast::<ImmutableString>().to_string())
2015 } else {
2016 Err(Box::new(EvalAltResult::ErrorRuntime(
2017 "primitive key must be a string".into(),
2018 Position::NONE,
2019 )))
2020 }
2021 })
2022 .transpose()?;
2023 primitives
2024 .create_node(&id, key, &props, generation.get())
2025 .map_err(|error| {
2026 Box::new(EvalAltResult::ErrorRuntime(
2027 error.to_string().into(),
2028 Position::NONE,
2029 ))
2030 })
2031 },
2032 );
2033 self.engine
2034 .register_static_module(namespace, module.clone().into());
2035 Ok(())
2036 }
2037
2038 #[must_use]
2039 pub fn primitive_registry(&self) -> PrimitiveRegistry {
2040 self.primitives.clone()
2041 }
2042
2043 pub fn register_native_handler(
2049 &self,
2050 descriptor: crate::NativeHandlerDescriptor,
2051 handler: impl FnMut(
2052 crate::NativeEvent,
2053 &mut UiRuntimeState,
2054 &mut gpui::Window,
2055 &mut gpui::App,
2056 ) -> Result<crate::EventResponse, String>
2057 + 'static,
2058 ) -> Result<(), crate::NativeHandlerError> {
2059 self.native_handlers.register(descriptor, handler)
2060 }
2061
2062 #[must_use]
2063 pub fn native_handler_registry(&self) -> crate::NativeHandlerRegistry {
2064 self.native_handlers.clone()
2065 }
2066
2067 #[must_use]
2068 pub fn syntax_registry(&self) -> crate::SyntaxRegistry {
2069 self.syntax_registry.clone()
2070 }
2071
2072 #[must_use]
2073 pub fn document_runtime_config(&self) -> crate::DocumentRuntimeConfig {
2074 self.document_runtime.clone()
2075 }
2076
2077 #[cfg(feature = "charts")]
2083 pub fn register_chart_transform(
2084 &self,
2085 id: impl Into<String>,
2086 transform: impl crate::HostChartTransform + 'static,
2087 ) -> Result<(), crate::ChartTransformError> {
2088 self.chart_transforms.register(id, transform)
2089 }
2090
2091 #[cfg(feature = "charts")]
2097 pub fn register_chart_map(&self, map: crate::ChartGeoMap) -> Result<(), crate::ChartGeoError> {
2098 self.chart_geo.register_map(map)
2099 }
2100
2101 #[cfg(feature = "charts")]
2107 pub fn register_chart_projection(
2108 &self,
2109 id: impl Into<String>,
2110 projection: impl crate::ChartGeoProjection + 'static,
2111 ) -> Result<(), crate::ChartGeoError> {
2112 self.chart_geo.register_projection(id, projection)
2113 }
2114
2115 #[cfg(feature = "charts")]
2121 pub fn register_chart_series(
2122 &self,
2123 id: impl Into<String>,
2124 renderer: impl crate::HostChartSeries + 'static,
2125 ) -> Result<(), crate::ChartSeriesExtensionError> {
2126 self.chart_series.register(id, renderer)
2127 }
2128
2129 #[cfg(feature = "charts")]
2135 pub fn register_chart_formatter(
2136 &self,
2137 id: impl Into<String>,
2138 formatter: impl crate::HostChartFormatter + 'static,
2139 ) -> Result<(), crate::ChartFormatterError> {
2140 self.chart_formatters.register(id, formatter)
2141 }
2142
2143 #[cfg(feature = "charts")]
2144 #[must_use]
2145 pub fn chart_transform_registry(&self) -> crate::ChartTransformRegistry {
2146 self.chart_transforms.clone()
2147 }
2148
2149 #[cfg(feature = "charts")]
2150 #[must_use]
2151 pub fn chart_geo_registry(&self) -> crate::ChartGeoRegistry {
2152 self.chart_geo.clone()
2153 }
2154
2155 #[cfg(feature = "charts")]
2156 #[must_use]
2157 pub fn chart_series_registry(&self) -> crate::ChartSeriesRegistry {
2158 self.chart_series.clone()
2159 }
2160
2161 #[cfg(feature = "charts")]
2162 #[must_use]
2163 pub fn chart_formatter_registry(&self) -> crate::ChartFormatterRegistry {
2164 self.chart_formatters.clone()
2165 }
2166
2167 pub fn set_slow_threshold(&mut self, threshold: Duration) {
2168 self.slow_threshold = threshold;
2169 }
2170
2171 pub fn set_operation_limit(&mut self, limit: u64) {
2183 self.operation_limit.set(limit.max(1));
2184 }
2185
2186 #[must_use]
2188 pub fn operation_limit(&self) -> u64 {
2189 self.operation_limit.get()
2190 }
2191
2192 pub fn set_expression_depth_limits(&mut self, global: usize, functions: usize) {
2196 self.engine
2197 .set_max_expr_depths(global.max(1), functions.max(1));
2198 }
2199
2200 #[must_use]
2201 pub fn expression_depth_limits(&self) -> (usize, usize) {
2202 (
2203 self.engine.max_expr_depth(),
2204 self.engine.max_function_expr_depth(),
2205 )
2206 }
2207
2208 #[must_use]
2209 pub fn take_timings(&self) -> Vec<ExecutionTiming> {
2210 std::mem::take(&mut *self.timings.borrow_mut())
2211 }
2212
2213 #[must_use]
2216 pub fn last_failed_timing(&self) -> Option<ExecutionTiming> {
2217 self.last_failed_timing.borrow().clone()
2218 }
2219
2220 #[must_use]
2221 pub(crate) fn last_failed_component(&self) -> Option<ComponentInstancePath> {
2222 self.last_failed_component.borrow().clone()
2223 }
2224
2225 fn record_timing(
2226 &self,
2227 operation: ExecutionOperation,
2228 source: &str,
2229 started: ExecutionTimingStart,
2230 succeeded: bool,
2231 ) {
2232 let duration = started.instant.elapsed();
2233 let operations = self.operation_total().saturating_sub(started.operations);
2234 let timing = ExecutionTiming {
2235 operation,
2236 source: source.to_owned(),
2237 duration,
2238 operations,
2239 operation_limit: started.operation_limit,
2240 round_operations: self.operation_total(),
2241 operation_semantics: OPERATION_SEMANTICS_VERSION,
2242 slow: duration >= self.slow_threshold,
2243 succeeded,
2244 };
2245 if !succeeded {
2246 *self.last_failed_timing.borrow_mut() = Some(timing.clone());
2247 }
2248 self.timings.borrow_mut().push(timing);
2249 }
2250
2251 pub(crate) fn begin_execution_session(&self) {
2252 self.begin_execution_session_from(0);
2253 }
2254
2255 fn begin_execution_session_from(&self, inherited_base: u64) {
2256 self.last_failed_timing.borrow_mut().take();
2257 self.last_failed_component.borrow_mut().take();
2258 self.operation_tracker.begin(inherited_base);
2259 }
2260
2261 fn align_execution_session_to(&self, inherited_base: u64) {
2262 self.operation_tracker.align(inherited_base);
2263 }
2264
2265 fn operation_total(&self) -> u64 {
2266 self.operation_tracker.total.get()
2267 }
2268
2269 fn begin_timing(&self) -> ExecutionTimingStart {
2270 ExecutionTimingStart {
2271 instant: Instant::now(),
2272 operations: self.operation_total(),
2273 operation_limit: self.operation_limit(),
2274 }
2275 }
2276
2277 fn candidate_generation(&mut self) -> ScriptGeneration {
2278 self.preparation_generation
2279 .unwrap_or_else(Self::allocate_generation)
2280 }
2281
2282 fn allocate_generation() -> ScriptGeneration {
2283 static NEXT_GENERATION: AtomicU64 = AtomicU64::new(1);
2284 ScriptGeneration(NEXT_GENERATION.fetch_add(1, Ordering::Relaxed))
2285 }
2286}
2287
2288pub(crate) fn validate_assignment_targets(ast: &AST) -> Result<(), RuntimeError> {
2289 let mut invalid = None;
2290 ast.walk(&mut |path| {
2291 let Some(ASTNode::Stmt(Stmt::Assignment(assignment))) = path.last() else {
2292 return true;
2293 };
2294 if !matches!(
2295 assignment.1.lhs,
2296 Expr::ThisPtr(_) | Expr::Variable(..) | Expr::Index(..) | Expr::Dot(..)
2297 ) {
2298 invalid = Some(assignment.1.lhs.position());
2299 return false;
2300 }
2301 true
2302 });
2303 invalid.map_or(Ok(()), |position| {
2304 Err(RuntimeError::InvalidAssignmentTarget(position))
2305 })
2306}
2307
2308fn validate_literal_imports(ast: &AST) -> Result<(), RuntimeError> {
2309 let mut dynamic = None;
2310 ast.walk(&mut |path| {
2311 let Some(ASTNode::Stmt(Stmt::Import(import, position))) = path.last() else {
2312 return true;
2313 };
2314 if matches!(import.0, Expr::StringConstant(..)) {
2315 true
2316 } else {
2317 dynamic = Some(*position);
2318 false
2319 }
2320 });
2321 dynamic.map_or(Ok(()), |position| {
2322 Err(RuntimeError::Import(format!(
2323 "Rhai imports must use a literal module string at {position}"
2324 )))
2325 })
2326}
2327
2328fn register_node_apis(engine: &mut Engine) {
2329 FuncRegistration::new("key_shortcut")
2332 .in_global_namespace()
2333 .register_into_engine(engine, |text: ImmutableString| {
2334 crate::ActionShortcut::from_text(text.as_str())
2335 .to_ui_value()
2336 .into_dynamic()
2337 });
2338 FuncRegistration::new("text")
2339 .in_global_namespace()
2340 .register_into_engine(engine, text_node);
2341 FuncRegistration::new("text")
2342 .in_global_namespace()
2343 .register_into_engine(engine, rich_text_node);
2344 FuncRegistration::new("span")
2345 .in_global_namespace()
2346 .register_into_engine(engine, span_value);
2347 FuncRegistration::new("canvas")
2348 .in_global_namespace()
2349 .register_into_engine(engine, canvas_node);
2350 FuncRegistration::new("svg")
2351 .in_global_namespace()
2352 .register_into_engine(engine, svg_node);
2353 FuncRegistration::new("box")
2354 .in_global_namespace()
2355 .register_into_engine(engine, box_node);
2356 FuncRegistration::new("fragment")
2357 .in_global_namespace()
2358 .register_into_engine(engine, fragment_node);
2359 FuncRegistration::new("motion_group")
2360 .in_global_namespace()
2361 .register_into_engine(engine, motion_group_node);
2362 FuncRegistration::new("stack")
2363 .in_global_namespace()
2364 .register_into_engine(engine, stack_node);
2365 FuncRegistration::new("handled")
2366 .in_global_namespace()
2367 .register_into_engine(engine, || crate::EventResponse::new().stop());
2368 FuncRegistration::new("propagate")
2369 .in_global_namespace()
2370 .register_into_engine(engine, crate::EventResponse::new);
2371 FuncRegistration::new("event_response")
2372 .in_global_namespace()
2373 .register_into_engine(engine, crate::EventResponse::new);
2374 FuncRegistration::new("column")
2375 .in_global_namespace()
2376 .register_into_engine(engine, column_node);
2377 FuncRegistration::new("row")
2378 .in_global_namespace()
2379 .register_into_engine(engine, row_node);
2380 FuncRegistration::new("error_boundary")
2381 .in_global_namespace()
2382 .register_into_engine(engine, error_boundary_node);
2383 FuncRegistration::new("error_boundary_lazy")
2384 .in_global_namespace()
2385 .register_into_engine(engine, lazy_error_boundary_node);
2386 FuncRegistration::new("asset")
2387 .in_global_namespace()
2388 .register_into_engine(engine, asset_id_from_script);
2389 FuncRegistration::new("image")
2390 .in_global_namespace()
2391 .register_into_engine(engine, image_node);
2392 FuncRegistration::new("image")
2393 .in_global_namespace()
2394 .register_into_engine(engine, asset_image_node);
2395 FuncRegistration::new("directional_image")
2396 .in_global_namespace()
2397 .register_into_engine(engine, directional_image_node);
2398 FuncRegistration::new("directional_image")
2399 .in_global_namespace()
2400 .register_into_engine(engine, directional_asset_image_node);
2401 FuncRegistration::new("image_source")
2402 .in_global_namespace()
2403 .register_into_engine(engine, generic_image_node);
2404 FuncRegistration::new("directional_image_source")
2405 .in_global_namespace()
2406 .register_into_engine(engine, generic_directional_image_node);
2407 FuncRegistration::new("overlay")
2408 .in_global_namespace()
2409 .register_into_engine(engine, overlay_node);
2410 FuncRegistration::new("layer")
2411 .in_global_namespace()
2412 .register_into_engine(engine, layer_node);
2413}
2414
2415fn register_native_handler_api(engine: &mut Engine, registry: &crate::NativeHandlerRegistry) {
2416 let registry = registry.clone();
2417 FuncRegistration::new("native_handler")
2418 .in_global_namespace()
2419 .register_into_engine(
2420 engine,
2421 move |id: ImmutableString| -> Result<crate::NativeHandlerRef, Box<EvalAltResult>> {
2422 let id = crate::NativeHandlerId::parse(id.to_string())
2423 .map_err(|error| Box::new(component_render_error(error.to_string())))?;
2424 registry
2425 .resolve(&id)
2426 .map_err(|error| Box::new(component_render_error(error.to_string())))
2427 },
2428 );
2429}
2430
2431pub(crate) const RHAI_MAX_ARRAY_SIZE: usize = 10_000;
2432pub(crate) const RHAI_MAX_MAP_SIZE: usize = 100_000;
2433pub(crate) const RHAI_MAX_STRING_SIZE: usize = 1_048_576;
2434pub(crate) const RHAI_MAX_DATA_DEPTH: usize = 64;
2435
2436fn configure_engine_limits(engine: &mut Engine) {
2437 engine.set_max_call_levels(64);
2438 engine.set_max_expr_depths(64, 32);
2442 engine.set_max_operations(0);
2446 engine.set_max_array_size(RHAI_MAX_ARRAY_SIZE);
2447 engine.set_max_map_size(RHAI_MAX_MAP_SIZE);
2448 engine.set_max_string_size(RHAI_MAX_STRING_SIZE);
2449}
2450
2451fn register_define_component_api(
2452 engine: &mut Engine,
2453 exports: &ComponentExportCollector,
2454 renderers: &ComponentRenderRegistry,
2455 generation: &Rc<Cell<ScriptGeneration>>,
2456) {
2457 let exports = exports.clone();
2458 let renderers = Rc::clone(renderers);
2459 let generation = Rc::clone(generation);
2460 FuncRegistration::new("define_component")
2461 .in_global_namespace()
2462 .register_into_engine(
2463 engine,
2464 move |mut raw: Map| -> Result<(), Box<EvalAltResult>> {
2465 let render = raw.remove("render").ok_or_else(|| {
2466 Box::new(component_render_error(
2467 "define_component requires a named `render` function",
2468 ))
2469 })?;
2470 if !render.is::<FnPtr>() {
2471 return Err(Box::new(component_render_error(
2472 "define_component `render` must be a function pointer",
2473 )));
2474 }
2475 let render = render.cast::<FnPtr>();
2476 if render.is_anonymous() || render.is_curried() {
2477 return Err(Box::new(component_render_error(
2478 "formal component render must be an uncurried named function",
2479 )));
2480 }
2481 if let Some(schema) = raw.get("schema")
2482 && schema.is::<Map>()
2483 && let Some(state) = schema.clone_cast::<Map>().get("state")
2484 {
2485 validate_state_default_encoding(state, "component.schema.state")?;
2486 }
2487 let decoded: crate::ComponentDefinition =
2488 rhai::serde::from_dynamic(&Dynamic::from_map(raw))?;
2489 let definition = crate::ComponentDefinition::new(decoded.metadata, decoded.schema)
2490 .map_err(|error| Box::new(component_render_error(error.to_string())))?;
2491 let id = definition.metadata.id.clone();
2492 exports
2493 .register_definition(definition)
2494 .map_err(|error| Box::new(component_render_error(error.to_string())))?;
2495 renderers
2496 .try_borrow_mut()
2497 .map_err(|_| {
2498 Box::new(component_render_error(
2499 "component render registry is already borrowed",
2500 ))
2501 })?
2502 .insert(
2503 id,
2504 RegisteredComponentRender {
2505 generation: generation.get(),
2506 render,
2507 },
2508 );
2509 Ok(())
2510 },
2511 );
2512}
2513
2514fn validate_state_default_encoding(raw: &Dynamic, path: &str) -> Result<(), Box<EvalAltResult>> {
2515 if !raw.is::<Map>() {
2516 return Ok(());
2517 }
2518 let raw = raw.clone_cast::<Map>();
2519 let Some(fields) = raw.get("fields").filter(|fields| fields.is::<Map>()) else {
2520 return Ok(());
2521 };
2522 for (name, field) in fields.clone_cast::<Map>() {
2523 if !field.is::<Map>() {
2524 continue;
2525 }
2526 let field = field.cast::<Map>();
2527 if let Some(value) = field.get("default") {
2528 rhai::serde::from_dynamic::<UiValue>(value).map_err(|source| {
2529 Box::new(component_render_error(format!(
2530 "{path}.fields.{name}.default: expected recursively tagged UiValue; map entries and array items each require type/value descriptors ({source})"
2531 )))
2532 })?;
2533 }
2534 }
2535 Ok(())
2536}
2537
2538fn register_component_runtime_apis(
2539 engine: &mut Engine,
2540 exports: &ComponentExportCollector,
2541 generation: &Rc<Cell<ScriptGeneration>>,
2542) -> (ActiveComponentRenderState, ComponentRenderRegistry) {
2543 let active = Rc::new(RefCell::new(None));
2544 let renderers = Rc::new(RefCell::new(BTreeMap::new()));
2545 register_define_component_api(engine, exports, &renderers, generation);
2546 register_render_component_api(engine, exports, &renderers, &active);
2547 register_effect_api(engine, &active);
2548 register_timer_api(engine, &active);
2549 register_signal_api(engine, &active);
2550 register_element_ref_api(engine, &active);
2551 register_virtual_collection_api(engine, &active);
2552 (active, renderers)
2553}
2554
2555fn register_render_component_api(
2556 engine: &mut Engine,
2557 exports: &ComponentExportCollector,
2558 renderers: &ComponentRenderRegistry,
2559 active: &ActiveComponentRenderState,
2560) {
2561 let exports = exports.clone();
2562 let renderers = Rc::clone(renderers);
2563 let active = Rc::clone(active);
2564 FuncRegistration::new("render_component")
2565 .in_global_namespace()
2566 .register_into_engine(
2567 engine,
2568 move |call: rhai::NativeCallContext<'_>,
2569 id: ImmutableString,
2570 props: Map|
2571 -> Result<UiNode, Box<EvalAltResult>> {
2572 let id = ModuleId::parse(id.to_string())
2573 .map_err(|error| Box::new(component_render_error(error.to_string())))?;
2574 let generation = active
2575 .try_borrow()
2576 .map_err(|_| {
2577 Box::new(component_render_error(
2578 "component render stack is already borrowed",
2579 ))
2580 })?
2581 .as_ref()
2582 .ok_or_else(|| {
2583 Box::new(component_render_error(
2584 "render_component may run only inside view",
2585 ))
2586 })?
2587 .generation;
2588 let registered = renderers
2589 .try_borrow()
2590 .map_err(|_| {
2591 Box::new(component_render_error(
2592 "component render registry is already borrowed",
2593 ))
2594 })?
2595 .get(&id)
2596 .cloned()
2597 .ok_or_else(|| {
2598 Box::new(component_render_error(format!(
2599 "component `{id}` has no registered render function"
2600 )))
2601 })?;
2602 if registered.generation != generation {
2603 return Err(Box::new(component_render_error(format!(
2604 "component `{id}` render belongs to stale generation {}",
2605 registered.generation
2606 ))));
2607 }
2608 execute_component_render(
2609 &call,
2610 &exports,
2611 &active,
2612 id.as_str().into(),
2613 props,
2614 ®istered.render,
2615 )
2616 },
2617 );
2618}
2619
2620#[allow(clippy::too_many_lines)] fn execute_component_render(
2622 call: &rhai::NativeCallContext<'_>,
2623 exports: &ComponentExportCollector,
2624 active: &ActiveComponentRenderState,
2625 id: ImmutableString,
2626 props: Map,
2627 render: &FnPtr,
2628) -> Result<UiNode, Box<EvalAltResult>> {
2629 let generation = active
2630 .try_borrow()
2631 .map_err(|_| {
2632 Box::new(component_render_error(
2633 "component render stack is already borrowed",
2634 ))
2635 })?
2636 .as_ref()
2637 .ok_or_else(|| {
2638 Box::new(component_render_error(
2639 "formal component rendering may run only inside view",
2640 ))
2641 })?
2642 .generation;
2643 let (component, mut invocation) = resolve_component_invocation(exports, id, props, generation)?;
2644 if render.is_anonymous() || render.is_curried() {
2645 return Err(Box::new(component_render_error(
2646 "formal component render must be an uncurried named function",
2647 )));
2648 }
2649 let part_styles = component_part_styles(&invocation.props);
2650 let recipe_props = invocation.retained_props.clone();
2651 let declared_key = invocation.key.clone();
2652 let render_recipe = render.clone();
2653 let (path, caller_context) = reserve_component_path(call, &component, &invocation, active)?;
2654 if let Some(node) = try_reuse_component_subtree(
2655 active,
2656 &path,
2657 &caller_context,
2658 &component,
2659 &invocation,
2660 render,
2661 )? {
2662 return Ok(node);
2663 }
2664 let native_context = crate::invocation::ScriptInvocationContext::capture_retained(call);
2665 let caller_binding = caller_component_binding(call, &caller_context);
2666 bind_component_callback_props(
2667 &mut invocation.props,
2668 &component.schema.props,
2669 &caller_binding,
2670 );
2671 bind_component_node_props(
2672 &mut invocation.props,
2673 &component.schema.props,
2674 &caller_binding,
2675 );
2676 let event_callbacks = component_event_callbacks(&component, &invocation.props);
2677 let recipe_event_callbacks = event_callbacks.clone();
2678 let script_props = invocation.props.clone();
2679 let context = enter_component_render(
2680 &path,
2681 &component,
2682 &invocation,
2683 native_context.clone(),
2684 active,
2685 )?;
2686 let recipe_component_context = context.clone();
2687 let result = render.call_within_context::<UiNode>(call, (context, invocation.props));
2688 if result.is_err() {
2689 record_component_render_failure(active, &path);
2690 }
2691 register_component_event_callbacks(
2692 active,
2693 &path,
2694 &caller_context,
2695 event_callbacks,
2696 &native_context,
2697 )?;
2698 let (environment, reusable) = component_render_metadata(active, &recipe_component_context)?;
2699 leave_component_render(active)?;
2700 let mut node = result?;
2701 node = node.with_owned_part_styles(part_styles.clone());
2702 let snapshot = crate::node::ComponentOwnedSnapshot::default();
2703 node = node.with_component_root_snapshot(path.clone(), snapshot.reference());
2704 node.bind_component_scope(
2705 &path,
2706 recipe_component_context.component_incarnation(),
2707 &component.schema.events,
2708 Some(&native_context),
2709 );
2710 register_component_invocation(
2711 active,
2712 ComponentInvocationRecipe {
2713 path: path.clone(),
2714 parent: caller_context.component_path().clone(),
2715 component: component.metadata.id.clone(),
2716 export: component.metadata.export.clone(),
2717 declared_key,
2718 props: recipe_props,
2719 script_props,
2720 component_context: recipe_component_context,
2721 caller_context: caller_context.clone(),
2722 part_styles,
2723 event_callbacks: recipe_event_callbacks,
2724 component_events: component.schema.events.clone(),
2725 declared_effects: component.schema.effects.clone(),
2726 render: render_recipe,
2727 snapshot,
2728 context: native_context.clone(),
2729 generation,
2730 environment,
2731 reusable,
2732 },
2733 )?;
2734 Ok(node)
2735}
2736
2737fn caller_component_binding(
2738 call: &rhai::NativeCallContext<'_>,
2739 caller_context: &UiContext,
2740) -> ComponentCallbackBinding {
2741 ComponentCallbackBinding {
2742 component: caller_context.callback_component_path().clone(),
2743 incarnation: caller_context.callback_component_incarnation(),
2744 events: caller_context.callback_event_schemas().clone(),
2745 context: Some(caller_context.native_context().cloned().unwrap_or_else(|| {
2746 crate::invocation::ScriptInvocationContext::capture_entry_retained(call)
2747 })),
2748 }
2749}
2750
2751fn component_render_metadata(
2752 active: &ActiveComponentRenderState,
2753 context: &UiContext,
2754) -> Result<(ComponentRenderEnvironment, bool), Box<EvalAltResult>> {
2755 let guard = active.try_borrow().map_err(|_| {
2756 Box::new(component_render_error(
2757 "component render stack is already borrowed",
2758 ))
2759 })?;
2760 let render = guard.as_ref().ok_or_else(|| {
2761 Box::new(component_render_error(
2762 "formal component rendering may run only inside view",
2763 ))
2764 })?;
2765 Ok((render.environment, context.component_render_is_reusable()))
2766}
2767
2768fn record_component_render_failure(
2769 active: &ActiveComponentRenderState,
2770 component: &ComponentInstancePath,
2771) {
2772 if let Ok(mut active) = active.try_borrow_mut()
2773 && let Some(active) = active.as_mut()
2774 && active.failed_component.is_none()
2775 {
2776 active.failed_component = Some(component.clone());
2777 }
2778}
2779
2780fn try_reuse_component_subtree(
2781 shared: &ActiveComponentRenderState,
2782 path: &ComponentInstancePath,
2783 caller_context: &UiContext,
2784 component: &crate::ComponentDefinition,
2785 invocation: &crate::ComponentInvocation,
2786 render: &FnPtr,
2787) -> Result<Option<UiNode>, Box<EvalAltResult>> {
2788 let mut guard = shared.try_borrow_mut().map_err(|_| {
2789 Box::new(component_render_error(
2790 "component render stack is already borrowed",
2791 ))
2792 })?;
2793 let active = guard.as_mut().ok_or_else(|| {
2794 Box::new(component_render_error(
2795 "formal component rendering may run only inside view",
2796 ))
2797 })?;
2798 let Some(scope) =
2799 reusable_component_scope(active, path, caller_context, component, invocation, render)
2800 else {
2801 return Ok(None);
2802 };
2803
2804 let reused_components = scope.invocations.len();
2805 for candidate in scope.invocations.keys() {
2806 let retained = active.transaction.retain_existing(candidate);
2807 debug_assert!(
2808 retained,
2809 "reuse candidates were checked against the state snapshot"
2810 );
2811 }
2812 active.seen.extend(scope.invocations.keys().cloned());
2813 active.invocations.extend(scope.invocations);
2814 active.event_handlers.extend(scope.event_handlers);
2815 active.effects.extend(scope.resources.effects);
2816 active.timers.extend(scope.resources.timers);
2817 active.signals.extend(scope.resources.signals);
2818 active.element_refs.extend(scope.resources.element_refs);
2819 active
2820 .virtual_collections
2821 .extend(scope.resources.virtual_collections);
2822 active
2823 .reused
2824 .push((component.metadata.id.to_string(), reused_components));
2825 Ok(Some(scope.node))
2826}
2827
2828fn prune_virtual_read_contributions(
2829 context: &UiContext,
2830 id: &crate::VirtualCollectionId,
2831 data: &crate::VirtualCollectionData,
2832 indices: &BTreeSet<usize>,
2833 retained: &UiNode,
2834) -> Result<(), RuntimeError> {
2835 let mut active = retained.virtual_read_contributions();
2836 active.extend(
2837 indices
2838 .iter()
2839 .filter_map(|index| data.key(*index))
2840 .map(
2841 |key| crate::read_dependency::ReadContribution::VirtualItem {
2842 collection: id.clone(),
2843 key: key.to_owned(),
2844 },
2845 ),
2846 );
2847 context
2848 .runtime()
2849 .try_borrow_mut()
2850 .map_err(|_| RuntimeError::ComponentRuntime("UI state is already borrowed".into()))?
2851 .retain_virtual_read_contributions(context.component_path(), &active);
2852 Ok(())
2853}
2854
2855fn retain_virtual_component_manifest(
2856 shared: &ActiveComponentRenderState,
2857) -> Result<(), RuntimeError> {
2858 let mut guard = shared.try_borrow_mut().map_err(|_| {
2859 RuntimeError::ComponentRuntime("component render stack is already borrowed".to_owned())
2860 })?;
2861 let active = guard.as_mut().ok_or_else(|| {
2862 RuntimeError::ComponentRuntime("virtual render has no active transaction".to_owned())
2863 })?;
2864 let Some(reuse) = active.reuse.as_ref() else {
2865 return Ok(());
2866 };
2867 let mut retained = reuse
2868 .invocations
2869 .keys()
2870 .filter(|path| reuse.subtrees.contains(path))
2871 .cloned()
2872 .collect::<BTreeSet<_>>();
2873 let mut children = BTreeMap::<ComponentInstancePath, Vec<ComponentInstancePath>>::new();
2876 for (path, recipe) in &reuse.invocations {
2877 children
2878 .entry(recipe.parent.clone())
2879 .or_default()
2880 .push(path.clone());
2881 }
2882 let mut pending = retained.iter().cloned().collect::<Vec<_>>();
2883 while let Some(parent) = pending.pop() {
2884 for child in children.get(&parent).into_iter().flatten() {
2885 if retained.insert(child.clone()) {
2886 pending.push(child.clone());
2887 }
2888 }
2889 }
2890 for path in &retained {
2891 if !active.transaction.retain_existing(path) {
2892 return Err(RuntimeError::ComponentRuntime(format!(
2893 "retained virtual component `{path}` has no committed state"
2894 )));
2895 }
2896 }
2897 active.seen.extend(retained.iter().cloned());
2898 active.invocations.extend(
2899 reuse
2900 .invocations
2901 .iter()
2902 .filter(|(path, _)| retained.contains(*path))
2903 .map(|(path, recipe)| (path.clone(), recipe.clone())),
2904 );
2905 active.event_handlers.extend(
2906 reuse
2907 .event_handlers
2908 .iter()
2909 .filter(|((path, _), _)| retained.contains(path))
2910 .map(|(id, callback)| (id.clone(), callback.clone())),
2911 );
2912 active.effects.extend(
2913 reuse
2914 .effects
2915 .iter()
2916 .filter(|(id, _)| retained.contains(id.component()))
2917 .map(|(id, descriptor)| (id.clone(), descriptor.clone())),
2918 );
2919 active.timers.extend(
2920 reuse
2921 .timers
2922 .iter()
2923 .filter(|(id, _)| retained.contains(id.component()))
2924 .map(|(id, descriptor)| (id.clone(), descriptor.clone())),
2925 );
2926 active.signals.extend(
2927 reuse
2928 .signals
2929 .iter()
2930 .filter(|(id, _)| retained.contains(id.component()))
2931 .map(|(id, descriptor)| (id.clone(), descriptor.clone())),
2932 );
2933 active.element_refs.extend(
2934 reuse
2935 .element_refs
2936 .iter()
2937 .filter(|id| retained.contains(id.component()))
2938 .cloned(),
2939 );
2940 active.virtual_collections.extend(
2941 reuse
2942 .virtual_collections
2943 .iter()
2944 .filter(|(id, _)| reuse.previous_root.virtual_collection_spec(id).is_some())
2945 .map(|(id, recipe)| (id.clone(), recipe.clone())),
2946 );
2947 Ok(())
2948}
2949
2950fn reusable_component_scope(
2951 active: &ActiveComponentRender,
2952 path: &ComponentInstancePath,
2953 caller_context: &UiContext,
2954 component: &crate::ComponentDefinition,
2955 invocation: &crate::ComponentInvocation,
2956 render: &FnPtr,
2957) -> Option<ReusedComponentScope> {
2958 let reuse = active.reuse.as_ref()?;
2959 if reuse.dirty.iter().any(|dirty| dirty.is_within(path)) {
2960 return None;
2961 }
2962 let recipe = reuse.invocations.get(path)?;
2963 if !recipe.reusable
2964 || recipe.generation != active.generation
2965 || recipe.environment != active.environment
2966 || recipe.parent != *caller_context.component_path()
2967 || recipe.component != component.metadata.id
2968 || recipe.export != component.metadata.export
2969 || recipe.declared_key != invocation.key
2970 || recipe.render.fn_name() != render.fn_name()
2971 || !recipe.props.reusable_eq(&invocation.retained_props)
2972 {
2973 return None;
2974 }
2975 let invocations = reuse
2976 .invocations
2977 .range(path.clone()..)
2978 .take_while(|(candidate, _)| candidate.is_within(path))
2979 .map(|(candidate, recipe)| (candidate.clone(), recipe.clone()))
2980 .collect::<BTreeMap<_, _>>();
2981 if invocations.is_empty()
2982 || invocations.values().any(|recipe| !recipe.reusable)
2983 || invocations
2984 .keys()
2985 .any(|candidate| !active.state_snapshot.contains_instance(candidate))
2986 {
2987 return None;
2988 }
2989 let node = if let Some(snapshot) = recipe.snapshot.current() {
2990 let mut node = snapshot.as_ref().clone();
2991 node.hydrate_component_subtrees();
2992 node
2993 } else {
2994 reuse.subtrees.get(&reuse.previous_root, path)?.clone()
2995 };
2996 Some(ReusedComponentScope {
2997 node,
2998 invocations,
2999 event_handlers: reuse
3000 .event_handlers
3001 .range((path.clone(), String::new())..)
3002 .take_while(|((candidate, _), _)| candidate.is_within(path))
3003 .map(|(id, callback)| (id.clone(), callback.clone()))
3004 .collect(),
3005 resources: reused_component_resources(reuse, path),
3006 })
3007}
3008
3009fn reused_component_resources(
3010 reuse: &ComponentReuseSnapshot,
3011 path: &ComponentInstancePath,
3012) -> ReusedComponentResources {
3013 ReusedComponentResources {
3014 effects: reuse
3015 .effects
3016 .iter()
3017 .filter(|(id, _)| id.component().is_within(path))
3018 .map(|(id, descriptor)| (id.clone(), descriptor.clone()))
3019 .collect(),
3020 timers: reuse
3021 .timers
3022 .iter()
3023 .filter(|(id, _)| id.component().is_within(path))
3024 .map(|(id, descriptor)| (id.clone(), descriptor.clone()))
3025 .collect(),
3026 signals: reuse
3027 .signals
3028 .iter()
3029 .filter(|(id, _)| id.component().is_within(path))
3030 .map(|(id, descriptor)| (id.clone(), descriptor.clone()))
3031 .collect(),
3032 element_refs: reuse
3033 .element_refs
3034 .iter()
3035 .filter(|id| id.component().is_within(path))
3036 .cloned()
3037 .collect(),
3038 virtual_collections: reuse
3039 .virtual_collections
3040 .iter()
3041 .filter(|(id, _)| id.component.is_within(path))
3042 .map(|(id, recipe)| (id.clone(), recipe.clone()))
3043 .collect(),
3044 }
3045}
3046
3047fn register_component_invocation(
3048 active: &ActiveComponentRenderState,
3049 recipe: ComponentInvocationRecipe,
3050) -> Result<(), Box<EvalAltResult>> {
3051 let mut guard = active.try_borrow_mut().map_err(|_| {
3052 Box::new(component_render_error(
3053 "component render stack is already borrowed",
3054 ))
3055 })?;
3056 let active = guard.as_mut().ok_or_else(|| {
3057 Box::new(component_render_error(
3058 "formal component rendering may run only inside view",
3059 ))
3060 })?;
3061 active.invocations.insert(recipe.path.clone(), recipe);
3062 Ok(())
3063}
3064
3065fn resolve_component_invocation(
3066 exports: &ComponentExportCollector,
3067 id: ImmutableString,
3068 props: Map,
3069 generation: ScriptGeneration,
3070) -> Result<(crate::ComponentDefinition, crate::ComponentInvocation), Box<EvalAltResult>> {
3071 let id: String = id.into();
3072 let id =
3073 ModuleId::parse(id).map_err(|error| Box::new(component_render_error(error.to_string())))?;
3074 let registry = exports
3075 .snapshot()
3076 .map_err(|error| Box::new(component_render_error(error.to_string())))?;
3077 let component = registry.get(&id).cloned().ok_or_else(|| {
3078 Box::new(component_render_error(format!(
3079 "component `{id}` is not exported"
3080 )))
3081 })?;
3082 let key = props.get("key").and_then(|value| {
3083 value
3084 .is::<ImmutableString>()
3085 .then(|| value.clone_cast::<ImmutableString>().to_string())
3086 });
3087 let invocation = component
3088 .invoke_with_generation(key, props, generation)
3089 .map_err(|error| Box::new(component_render_error(error.to_string())))?;
3090 Ok((component, invocation))
3091}
3092
3093fn reserve_component_path(
3094 call: &rhai::NativeCallContext<'_>,
3095 component: &crate::ComponentDefinition,
3096 invocation: &crate::ComponentInvocation,
3097 shared: &ActiveComponentRenderState,
3098) -> Result<(ComponentInstancePath, UiContext), Box<EvalAltResult>> {
3099 let mut guard = shared.try_borrow_mut().map_err(|_| {
3100 Box::new(component_render_error(
3101 "component render stack is already borrowed",
3102 ))
3103 })?;
3104 let active = guard.as_mut().ok_or_else(|| {
3105 Box::new(component_render_error(
3106 "formal component rendering may run only inside view",
3107 ))
3108 })?;
3109 let caller_context = active.contexts.last().cloned().ok_or_else(|| {
3110 Box::new(component_render_error(
3111 "component render context stack has no caller",
3112 ))
3113 })?;
3114 let parent = active.stack.last().cloned().ok_or_else(|| {
3115 Box::new(component_render_error(
3116 "component render stack has no application root",
3117 ))
3118 })?;
3119 let key = invocation.key.clone().unwrap_or_else(|| {
3120 let callsite = format!(
3121 "{}@{}",
3122 call.call_source().unwrap_or("<script>"),
3123 call.call_position()
3124 );
3125 let occurrence = active
3126 .positions
3127 .entry(format!("{parent}/{}/{callsite}", component.metadata.export))
3128 .or_default();
3129 let key = format!("{callsite}#{occurrence}");
3130 *occurrence = occurrence.saturating_add(1);
3131 key
3132 });
3133 let path = parent.child(&component.metadata.export, key);
3134 if !active.seen.insert(path.clone()) {
3135 return Err(Box::new(component_render_error(format!(
3136 "duplicate component instance path `{path}`; add a stable key"
3137 ))));
3138 }
3139 Ok((path, caller_context))
3140}
3141
3142fn enter_component_render(
3143 path: &ComponentInstancePath,
3144 component: &crate::ComponentDefinition,
3145 invocation: &crate::ComponentInvocation,
3146 native_context: crate::invocation::ScriptInvocationContext,
3147 shared: &ActiveComponentRenderState,
3148) -> Result<UiContext, Box<EvalAltResult>> {
3149 let mut guard = shared.try_borrow_mut().map_err(|_| {
3150 Box::new(component_render_error(
3151 "component render stack is already borrowed",
3152 ))
3153 })?;
3154 let active = guard.as_mut().ok_or_else(|| {
3155 Box::new(component_render_error(
3156 "formal component rendering may run only inside view",
3157 ))
3158 })?;
3159 active
3160 .transaction
3161 .mount(path.clone(), &component.schema.state)
3162 .map_err(|error| Box::new(component_render_error(error.to_string())))?;
3163 let global_part_styles = {
3164 let mut runtime = active
3165 .root_context
3166 .runtime()
3167 .try_borrow_mut()
3168 .map_err(|_| Box::new(component_render_error("UI state is already borrowed")))?;
3169 runtime
3170 .component_state
3171 .mount_instance(path.clone(), &component.schema.state)
3172 .map_err(|error| Box::new(component_render_error(error.to_string())))?;
3173 runtime
3174 .component_styles()
3175 .component(&component.metadata.id)
3176 .cloned()
3177 .unwrap_or_default()
3178 };
3179 let context = active
3180 .root_context
3181 .for_component(path.clone(), component.schema.events.clone())
3182 .with_component_styles(
3183 global_part_styles,
3184 component_root_style(&invocation.props),
3185 component_part_styles(&invocation.props),
3186 )
3187 .with_native_context(Some(native_context))
3188 .with_generation(active.generation);
3189 active.stack.push(path.clone());
3190 active.contexts.push(context.clone());
3191 active
3192 .effect_keys
3193 .push(Some(component.schema.effects.clone()));
3194 Ok(context)
3195}
3196
3197fn leave_component_render(active: &ActiveComponentRenderState) -> Result<(), Box<EvalAltResult>> {
3198 let mut guard = active.try_borrow_mut().map_err(|_| {
3199 Box::new(component_render_error(
3200 "component render stack is already borrowed",
3201 ))
3202 })?;
3203 if let Some(active) = guard.as_mut() {
3204 active.stack.pop();
3205 active.contexts.pop();
3206 active.effect_keys.pop();
3207 }
3208 Ok(())
3209}
3210
3211fn register_effect_api(engine: &mut Engine, active: &ActiveComponentRenderState) {
3212 let active = Rc::clone(active);
3213 FuncRegistration::new("effect")
3214 .in_global_namespace()
3215 .register_into_engine(
3216 engine,
3217 move |call: rhai::NativeCallContext<'_>,
3218 key: ImmutableString,
3219 dependencies: Dynamic,
3220 start: FnPtr,
3221 cleanup: FnPtr|
3222 -> Result<(), Box<EvalAltResult>> {
3223 let key = key.to_string();
3224 let (component, context, generation, declared) = {
3225 let guard = active.try_borrow().map_err(|_| {
3226 Box::new(component_render_error(
3227 "component render stack is already borrowed",
3228 ))
3229 })?;
3230 let render = guard.as_ref().ok_or_else(|| {
3231 Box::new(component_render_error(
3232 "effect may run only during formal component render",
3233 ))
3234 })?;
3235 let context = render.contexts.last().cloned().ok_or_else(|| {
3236 Box::new(component_render_error(
3237 "component render context stack is empty",
3238 ))
3239 })?;
3240 let declared =
3241 render
3242 .effect_keys
3243 .last()
3244 .cloned()
3245 .flatten()
3246 .ok_or_else(|| {
3247 Box::new(component_render_error(
3248 "effects may be declared only by formal components",
3249 ))
3250 })?;
3251 (
3252 context.component_path().clone(),
3253 context,
3254 render.generation,
3255 declared,
3256 )
3257 };
3258 if !declared.contains(&key) {
3259 return Err(Box::new(component_render_error(
3260 crate::EffectError::Undeclared { component, key }.to_string(),
3261 )));
3262 }
3263 let id = crate::EffectId::new(component.clone(), key.clone())
3264 .map_err(|error| Box::new(component_render_error(error.to_string())))?;
3265 let dependencies = UiValue::from_dynamic(dependencies)
3266 .map_err(|error| Box::new(component_render_error(error.to_string())))?;
3267 let native_context =
3268 crate::invocation::ScriptInvocationContext::capture_retained(&call);
3269 let mut start = ScriptCallback::try_from_fn_ptr(start, generation)
3270 .map_err(|error| Box::new(component_render_error(error.to_string())))?;
3271 start.bind_component_scope_if_unset(
3272 &component,
3273 context.component_incarnation(),
3274 context.event_schemas().clone(),
3275 );
3276 start.bind_native_context_if_unset(native_context.clone());
3277 let mut cleanup = ScriptCallback::try_from_fn_ptr(cleanup, generation)
3278 .map_err(|error| Box::new(component_render_error(error.to_string())))?;
3279 cleanup.bind_component_scope_if_unset(
3280 &component,
3281 context.component_incarnation(),
3282 context.event_schemas().clone(),
3283 );
3284 cleanup.bind_native_context_if_unset(native_context);
3285 let descriptor =
3286 crate::EffectDescriptor::new(id.clone(), dependencies, start, cleanup);
3287 let mut guard = active.try_borrow_mut().map_err(|_| {
3288 Box::new(component_render_error(
3289 "component render stack is already borrowed",
3290 ))
3291 })?;
3292 let render = guard.as_mut().ok_or_else(|| {
3293 Box::new(component_render_error(
3294 "effect may run only during formal component render",
3295 ))
3296 })?;
3297 if render.effects.insert(id, descriptor).is_some() {
3298 return Err(Box::new(component_render_error(
3299 crate::EffectError::Duplicate { component, key }.to_string(),
3300 )));
3301 }
3302 Ok(())
3303 },
3304 );
3305}
3306
3307fn register_timer_api(engine: &mut Engine, active: &ActiveComponentRenderState) {
3308 let active = Rc::clone(active);
3309 FuncRegistration::new("timeout")
3310 .in_global_namespace()
3311 .register_into_engine(
3312 engine,
3313 move |call: rhai::NativeCallContext<'_>,
3314 key: ImmutableString,
3315 delay_ms: rhai::INT,
3316 paused: bool,
3317 callback: FnPtr,
3318 payload: Dynamic|
3319 -> Result<(), Box<EvalAltResult>> {
3320 let (component, context, generation) = {
3321 let guard = active.try_borrow().map_err(|_| {
3322 Box::new(component_render_error(
3323 "component render stack is already borrowed",
3324 ))
3325 })?;
3326 let render = guard.as_ref().ok_or_else(|| {
3327 Box::new(component_render_error(
3328 "timeout may run only during formal component render",
3329 ))
3330 })?;
3331 let context = render.contexts.last().cloned().ok_or_else(|| {
3332 Box::new(component_render_error(
3333 "component render context stack is empty",
3334 ))
3335 })?;
3336 (context.component_path().clone(), context, render.generation)
3337 };
3338 let delay_ms = u64::try_from(delay_ms).map_err(|_| {
3339 Box::new(component_render_error(
3340 "timeout delay must be a positive integer",
3341 ))
3342 })?;
3343 let id = crate::TimerId::new(component.clone(), key.to_string())
3344 .map_err(|error| Box::new(component_render_error(error.to_string())))?;
3345 let payload = UiValue::from_dynamic(payload)
3346 .map_err(|error| Box::new(component_render_error(error.to_string())))?;
3347 let native_context =
3348 crate::invocation::ScriptInvocationContext::capture_retained(&call);
3349 let mut callback = ScriptCallback::try_from_fn_ptr(callback, generation)
3350 .map_err(|error| Box::new(component_render_error(error.to_string())))?;
3351 callback.bind_component_scope_if_unset(
3352 &component,
3353 context.component_incarnation(),
3354 context.event_schemas().clone(),
3355 );
3356 callback.bind_native_context_if_unset(native_context);
3357 let descriptor = crate::TimerDescriptor::new(
3358 id.clone(),
3359 Duration::from_millis(delay_ms),
3360 paused,
3361 callback,
3362 payload,
3363 )
3364 .map_err(|error| Box::new(component_render_error(error.to_string())))?;
3365 let mut guard = active.try_borrow_mut().map_err(|_| {
3366 Box::new(component_render_error(
3367 "component render stack is already borrowed",
3368 ))
3369 })?;
3370 let render = guard.as_mut().ok_or_else(|| {
3371 Box::new(component_render_error(
3372 "timeout may run only during formal component render",
3373 ))
3374 })?;
3375 if render.timers.insert(id.clone(), descriptor).is_some() {
3376 return Err(Box::new(component_render_error(format!(
3377 "timeout `{}` is declared more than once in `{}`",
3378 id.key(),
3379 id.component()
3380 ))));
3381 }
3382 Ok(())
3383 },
3384 );
3385}
3386
3387fn register_signal_api(engine: &mut Engine, active: &ActiveComponentRenderState) {
3388 let scalar_active = Rc::clone(active);
3389 FuncRegistration::new("signal")
3390 .in_global_namespace()
3391 .register_into_engine(
3392 engine,
3393 move |key: ImmutableString,
3394 initial: Dynamic|
3395 -> Result<crate::NativeSignal, Box<EvalAltResult>> {
3396 let key = key.to_string();
3397 let initial = crate::SignalValue::from_dynamic(initial)
3398 .map_err(|error| Box::new(crate::signal::signal_runtime_error(&error)))?;
3399 declare_signal(&scalar_active, key, initial)
3400 },
3401 );
3402
3403 let optional_active = Rc::clone(active);
3404 FuncRegistration::new("optional_float_signal")
3405 .in_global_namespace()
3406 .register_into_engine(
3407 engine,
3408 move |key: ImmutableString| -> Result<crate::NativeSignal, Box<EvalAltResult>> {
3409 declare_signal(
3410 &optional_active,
3411 key.to_string(),
3412 crate::SignalValue::OptionalFloat(None),
3413 )
3414 },
3415 );
3416
3417 let parent_active = Rc::clone(active);
3418 FuncRegistration::new("bind_parent_signal")
3419 .in_global_namespace()
3420 .register_into_engine(
3421 engine,
3422 move |node: &mut UiNode,
3423 context: UiContext,
3424 property: ImmutableString,
3425 key: ImmutableString|
3426 -> Result<UiNode, Box<EvalAltResult>> {
3427 let property = crate::SignalProperty::parse(property.as_str())
3428 .map_err(|error| Box::new(crate::signal::signal_runtime_error(&error)))?;
3429 if property.signal_kind() != crate::SignalKind::OptionalFloat {
3430 return Err(Box::new(crate::signal::signal_runtime_error(
3431 &"parent signal binding currently supports optional-float properties",
3432 )));
3433 }
3434 let pending = parent_active
3437 .try_borrow()
3438 .ok()
3439 .and_then(|guard| {
3440 guard
3441 .as_ref()
3442 .map(|render| render.signals.keys().cloned().collect::<Vec<_>>())
3443 })
3444 .unwrap_or_default();
3445 let signal = context
3446 .parent_optional_float_signal_ref(key.as_str(), &pending)
3447 .map_err(|error| Box::new(crate::signal::signal_runtime_error(&error)))?;
3448 node.clone()
3449 .with_signal_binding(property, signal)
3450 .map_err(|error| Box::new(crate::signal::signal_runtime_error(&error)))
3451 },
3452 );
3453}
3454
3455fn declare_signal(
3456 active: &ActiveComponentRenderState,
3457 key: String,
3458 initial: crate::SignalValue,
3459) -> Result<crate::NativeSignal, Box<EvalAltResult>> {
3460 let mut guard = active.try_borrow_mut().map_err(|_| {
3461 Box::new(crate::signal::signal_runtime_error(
3462 &"component render stack is already borrowed",
3463 ))
3464 })?;
3465 let render = guard.as_mut().ok_or_else(|| {
3466 Box::new(crate::signal::signal_runtime_error(
3467 &"signal may run only during formal component render",
3468 ))
3469 })?;
3470 if render.effect_keys.last().and_then(Option::as_ref).is_none() {
3471 return Err(Box::new(crate::signal::signal_runtime_error(
3472 &"signals may be declared only by formal components",
3473 )));
3474 }
3475 let context = render.contexts.last().ok_or_else(|| {
3476 Box::new(crate::signal::signal_runtime_error(
3477 &"component render context stack is empty",
3478 ))
3479 })?;
3480 let component = context.component_path().clone();
3481 let incarnation = context.component_incarnation();
3482 if render
3483 .signals
3484 .keys()
3485 .any(|id| id.component() == &component && id.key() == key)
3486 {
3487 return Err(Box::new(crate::signal::signal_runtime_error(
3488 &crate::SignalError::Duplicate { component, key },
3489 )));
3490 }
3491 let id = crate::SignalId::new_scoped(component, incarnation, key, initial.kind())
3492 .map_err(|error| Box::new(crate::signal::signal_runtime_error(&error)))?;
3493 let signal = crate::NativeSignal::new(id.clone());
3494 render
3495 .signals
3496 .insert(id, crate::signal::SignalDescriptor::new(initial));
3497 Ok(signal)
3498}
3499
3500fn register_element_ref_api(engine: &mut Engine, active: &ActiveComponentRenderState) {
3501 let active = Rc::clone(active);
3502 FuncRegistration::new("element_ref")
3503 .in_global_namespace()
3504 .register_into_engine(
3505 engine,
3506 move |key: ImmutableString| -> Result<crate::ElementRef, Box<EvalAltResult>> {
3507 let key = key.to_string();
3508 let mut guard = active.try_borrow_mut().map_err(|_| {
3509 Box::new(component_render_error(
3510 "component render stack is already borrowed",
3511 ))
3512 })?;
3513 let render = guard.as_mut().ok_or_else(|| {
3514 Box::new(component_render_error(
3515 "element_ref may run only during formal component render",
3516 ))
3517 })?;
3518 if render.effect_keys.last().and_then(Option::as_ref).is_none() {
3519 return Err(Box::new(component_render_error(
3520 "element refs may be declared only by formal components",
3521 )));
3522 }
3523 let component = render
3524 .contexts
3525 .last()
3526 .ok_or_else(|| {
3527 Box::new(component_render_error(
3528 "component render context stack is empty",
3529 ))
3530 })?
3531 .component_path()
3532 .clone();
3533 let id = crate::ElementRefId::new(component.clone(), key.clone())
3534 .map_err(|error| Box::new(component_render_error(error.to_string())))?;
3535 if !render.element_refs.insert(id.clone()) {
3536 return Err(Box::new(component_render_error(format!(
3537 "element ref `{key}` is declared more than once by component `{component}`"
3538 ))));
3539 }
3540 Ok(crate::ElementRef::new(id))
3541 },
3542 );
3543}
3544
3545#[derive(Debug)]
3546struct DecodedVirtualCollection {
3547 key: String,
3548 label: String,
3549 data: crate::VirtualCollectionData,
3550 estimated_height: f64,
3551 height: Option<f64>,
3552 overdraw_pixels: f64,
3553 bottom_align: bool,
3554 follow_tail: bool,
3555 reveal_key: Option<String>,
3556 sticky_headers: Arc<BTreeSet<usize>>,
3557}
3558
3559impl DecodedVirtualCollection {
3560 fn into_spec(
3561 self,
3562 id: crate::VirtualCollectionId,
3563 realized: BTreeMap<usize, UiNode>,
3564 ) -> crate::VirtualCollectionNodeSpec {
3565 crate::VirtualCollectionNodeSpec {
3566 id,
3567 label: self.label,
3568 data: self.data,
3569 realized,
3570 estimated_height: self.estimated_height,
3571 height: self.height,
3572 overdraw_pixels: self.overdraw_pixels,
3573 bottom_align: self.bottom_align,
3574 follow_tail: self.follow_tail,
3575 reveal_key: self.reveal_key,
3576 sticky_headers: self.sticky_headers,
3577 inherited_motion_group: None,
3578 inherited_motion_scope: None,
3579 }
3580 }
3581}
3582
3583fn decode_virtual_collection(
3584 mut config: Map,
3585) -> Result<DecodedVirtualCollection, Box<EvalAltResult>> {
3586 let key = collection_string(&mut config, "key")?;
3587 let label = collection_optional_string(&mut config, "label")?.unwrap_or_default();
3588 let estimated_height = collection_positive(&mut config, "estimated_height")?;
3589 let height = collection_optional_number(&mut config, "height")?;
3590 let fill_height = collection_optional_bool(&mut config, "fill_height")?.unwrap_or(false);
3591 let height = match (height, fill_height) {
3592 (Some(_), true) => {
3593 return Err(Box::new(component_render_error(
3594 "virtual collection must specify either `height` or `fill_height`, not both",
3595 )));
3596 }
3597 (None, false) => {
3598 return Err(Box::new(component_render_error(
3599 "virtual collection requires `height` or `fill_height: true`",
3600 )));
3601 }
3602 (None, true) => None,
3603 (Some(height), false) if height.is_finite() && height > 0.0 => Some(height),
3604 (Some(_), false) => {
3605 return Err(Box::new(component_render_error(
3606 "virtual collection `height` must be finite and positive",
3607 )));
3608 }
3609 };
3610 let overdraw_pixels = collection_optional_number(&mut config, "overdraw_pixels")?
3611 .unwrap_or(estimated_height * 2.0);
3612 if !overdraw_pixels.is_finite() || !(0.0..=10_000.0).contains(&overdraw_pixels) {
3613 return Err(Box::new(component_render_error(
3614 "virtual collection overdraw_pixels must be between 0 and 10000",
3615 )));
3616 }
3617 let alignment =
3618 collection_optional_string(&mut config, "alignment")?.unwrap_or_else(|| "top".to_owned());
3619 let bottom_align = match alignment.as_str() {
3620 "top" => false,
3621 "bottom" => true,
3622 _ => {
3623 return Err(Box::new(component_render_error(
3624 "virtual collection alignment must be `top` or `bottom`",
3625 )));
3626 }
3627 };
3628 let follow_tail = collection_optional_bool(&mut config, "follow_tail")?.unwrap_or(false);
3629 let data = collection_data(&mut config)?;
3630 let reveal_key = collection_optional_string(&mut config, "reveal_key")?;
3631 if reveal_key.as_deref() == Some("") {
3632 return Err(Box::new(component_render_error(
3633 "virtual collection `reveal_key` must not be empty",
3634 )));
3635 }
3636 if let Some(reveal_key) = reveal_key.as_deref()
3637 && !(0..data.len()).any(|index| data.key(index) == Some(reveal_key))
3638 {
3639 return Err(Box::new(component_render_error(format!(
3640 "virtual collection reveal key `{reveal_key}` is not present in its data"
3641 ))));
3642 }
3643 if follow_tail && reveal_key.is_some() {
3644 return Err(Box::new(component_render_error(
3645 "virtual collection must not combine `follow_tail` with `reveal_key`",
3646 )));
3647 }
3648 let sticky_headers = collection_optional_indices(&mut config, "sticky_headers")?
3649 .map_or_else(|| data.sticky_headers(), Arc::new);
3650 if let Some(index) = sticky_headers.iter().find(|index| **index >= data.len()) {
3651 return Err(Box::new(component_render_error(format!(
3652 "virtual collection sticky header index {index} is outside {} items",
3653 data.len()
3654 ))));
3655 }
3656 if bottom_align && !sticky_headers.is_empty() {
3657 return Err(Box::new(component_render_error(
3658 "sticky headers require top-aligned virtual collections",
3659 )));
3660 }
3661 if let Some((unknown, _)) = config.into_iter().next() {
3662 return Err(Box::new(component_render_error(format!(
3663 "unknown virtual collection field `{unknown}`"
3664 ))));
3665 }
3666 Ok(DecodedVirtualCollection {
3667 key,
3668 label,
3669 data,
3670 estimated_height,
3671 height,
3672 overdraw_pixels,
3673 bottom_align,
3674 follow_tail,
3675 reveal_key,
3676 sticky_headers,
3677 })
3678}
3679
3680fn register_virtual_collection_api(engine: &mut Engine, active: &ActiveComponentRenderState) {
3681 let active = Rc::clone(active);
3682 FuncRegistration::new("virtual_collection")
3683 .in_global_namespace()
3684 .register_into_engine(
3685 engine,
3686 move |call: rhai::NativeCallContext<'_>,
3687 config: Map,
3688 renderer: FnPtr|
3689 -> Result<UiNode, Box<EvalAltResult>> {
3690 validate_virtual_renderer(&renderer)?;
3691 let decoded = decode_virtual_collection(config)?;
3692
3693 let VirtualCollectionContext {
3694 component,
3695 context,
3696 generation,
3697 } = virtual_collection_context(&active)?;
3698 let id = crate::VirtualCollectionId {
3699 component: component.clone(),
3700 key: decoded.key.clone(),
3701 };
3702 let collection_context = context
3703 .for_structural_scope(
3704 component.child("VirtualCollection", decoded.key.clone()),
3705 BTreeMap::new(),
3706 )
3707 .with_generation(generation);
3708 let native_context =
3709 crate::invocation::ScriptInvocationContext::capture_retained(&call);
3710 let realized = realize_seeded_virtual_collection(
3711 &call,
3712 &renderer,
3713 &collection_context,
3714 &active,
3715 &id,
3716 &decoded,
3717 )?;
3718 let recipe = VirtualCollectionRecipe {
3719 id: id.clone(),
3720 data: decoded.data.clone(),
3721 renderer,
3722 renderer_context: native_context,
3723 context: collection_context,
3724 generation,
3725 };
3726 let mut guard = active.try_borrow_mut().map_err(|_| {
3727 Box::new(component_render_error(
3728 "component render stack is already borrowed",
3729 ))
3730 })?;
3731 let render = guard.as_mut().ok_or_else(|| {
3732 Box::new(component_render_error(
3733 "virtual_collection may run only during view render",
3734 ))
3735 })?;
3736 if render
3737 .virtual_collections
3738 .insert(id.clone(), recipe)
3739 .is_some()
3740 {
3741 return Err(Box::new(component_render_error(format!(
3742 "virtual collection `{}` is declared more than once in `{}`",
3743 decoded.key, id.component
3744 ))));
3745 }
3746 Ok(UiNode::virtual_collection(decoded.into_spec(id, realized)))
3747 },
3748 );
3749}
3750
3751fn validate_virtual_renderer(renderer: &FnPtr) -> Result<(), Box<EvalAltResult>> {
3752 if renderer.is_anonymous() {
3753 return Err(Box::new(component_render_error(
3754 "virtual collection item renderer must be a named function",
3755 )));
3756 }
3757 for (index, value) in renderer.iter_curry().enumerate() {
3758 validate_virtual_renderer_curry(value).map_err(|error| {
3759 Box::new(component_render_error(format!(
3760 "virtual collection renderer curry {index} is not durable presentation data: {error}"
3761 )))
3762 })?;
3763 }
3764 Ok(())
3765}
3766
3767fn validate_virtual_renderer_curry(value: &Dynamic) -> Result<(), crate::UiValueError> {
3768 if value.is::<UiNode>() || value.is::<crate::NativeSignal>() {
3769 return Ok(());
3770 }
3771 if value.is::<Array>() {
3772 for value in value.clone_cast::<Array>() {
3773 validate_virtual_renderer_curry(&value)?;
3774 }
3775 return Ok(());
3776 }
3777 if value.is::<Map>() {
3778 for value in value.clone_cast::<Map>().values() {
3779 validate_virtual_renderer_curry(value)?;
3780 }
3781 return Ok(());
3782 }
3783 UiValue::from_dynamic(value.clone()).map(|_| ())
3784}
3785
3786fn virtual_item_context(
3790 context: &UiContext,
3791 id: &crate::VirtualCollectionId,
3792 key: &str,
3793) -> UiContext {
3794 context
3795 .for_structural_scope(context.component_path().child("Item", key), BTreeMap::new())
3796 .for_virtual_item(id, key)
3797}
3798
3799fn realize_initial_collection(
3800 call: &rhai::NativeCallContext<'_>,
3801 renderer: &FnPtr,
3802 context: &UiContext,
3803 active: &ActiveComponentRenderState,
3804 id: &crate::VirtualCollectionId,
3805 data: &crate::VirtualCollectionData,
3806 indices: &BTreeSet<usize>,
3807) -> Result<BTreeMap<usize, UiNode>, Box<EvalAltResult>> {
3808 let mut realized = BTreeMap::new();
3809 for index in indices.iter().copied().filter(|index| *index < data.len()) {
3810 let item = data
3811 .item(index)
3812 .map_err(|error| Box::new(component_render_error(error.to_string())))?
3813 .ok_or_else(|| {
3814 Box::new(component_render_error(format!(
3815 "virtual collection item {index} disappeared"
3816 )))
3817 })?;
3818 let (item_key, mut payload) = collection_payload(&item, index)?;
3819 add_collection_neighbors(data, index, &mut payload);
3820 let item_context = virtual_item_context(context, id, &item_key);
3821 enter_virtual_collection_scope(active, item_context.clone())?;
3822 let node = renderer.call_within_context::<UiNode>(call, (item_context, payload));
3823 leave_component_render(active)?;
3824 realized.insert(index, node?.with_key(item_key));
3825 }
3826 Ok(realized)
3827}
3828
3829fn realize_seeded_virtual_collection(
3830 call: &rhai::NativeCallContext<'_>,
3831 renderer: &FnPtr,
3832 context: &UiContext,
3833 active: &ActiveComponentRenderState,
3834 id: &crate::VirtualCollectionId,
3835 decoded: &DecodedVirtualCollection,
3836) -> Result<BTreeMap<usize, UiNode>, Box<EvalAltResult>> {
3837 let previous_metrics = context
3838 .runtime()
3839 .try_borrow()
3840 .map_err(|_| {
3841 Box::new(component_render_error(
3842 "UI state is already borrowed while seeding a virtual collection",
3843 ))
3844 })?
3845 .virtual_requests
3846 .metrics(id);
3847 let viewport = decoded.height.unwrap_or_else(|| {
3848 previous_metrics
3849 .as_ref()
3850 .map_or(decoded.estimated_height, |metrics| {
3851 if metrics.viewport_height > 0.0 {
3852 metrics.viewport_height
3853 } else {
3854 decoded.estimated_height
3855 }
3856 })
3857 });
3858 let count = nonnegative_usize(
3859 ((viewport + decoded.overdraw_pixels) / decoded.estimated_height).ceil() + 1.0,
3860 )
3861 .min(decoded.data.len());
3862 let indices =
3863 virtual_collection_seed_indices(active, id, decoded, count, previous_metrics.as_ref())?;
3864 enter_virtual_collection_scope(active, context.clone())?;
3865 let realized =
3866 realize_initial_collection(call, renderer, context, active, id, &decoded.data, &indices);
3867 leave_component_render(active)?;
3868 realized
3869}
3870
3871fn virtual_collection_seed_indices(
3872 active: &ActiveComponentRenderState,
3873 id: &crate::VirtualCollectionId,
3874 decoded: &DecodedVirtualCollection,
3875 count: usize,
3876 metrics: Option<&crate::VirtualCollectionMetrics>,
3877) -> Result<BTreeSet<usize>, Box<EvalAltResult>> {
3878 let mut indices = BTreeSet::new();
3879 let guard = active.try_borrow().map_err(|_| {
3880 Box::new(component_render_error(
3881 "component render stack is already borrowed",
3882 ))
3883 })?;
3884 let reuse = guard.as_ref().and_then(|render| render.reuse.as_ref());
3885 let previous = reuse.and_then(|reuse| reuse.previous_root.virtual_collection_spec(id));
3886 let preserves_scroll = previous.is_some_and(|previous| {
3887 previous.estimated_height.to_bits() == decoded.estimated_height.to_bits()
3888 && previous.overdraw_pixels.to_bits() == decoded.overdraw_pixels.to_bits()
3889 && previous.bottom_align == decoded.bottom_align
3890 && same_collection_key_order(&previous.data, &decoded.data)
3891 });
3892
3893 if preserves_scroll {
3894 if let Some(previous) = previous {
3895 indices.extend(previous.realized.keys().copied());
3896 }
3897 if let Some(metrics) = metrics {
3898 extend_virtual_window(
3899 &mut indices,
3900 metrics.scroll_item,
3901 count,
3902 decoded.data.len(),
3903 virtual_overdraw_items(decoded),
3904 );
3905 }
3906 } else if decoded.reveal_key.is_none() && !decoded.bottom_align && !decoded.follow_tail {
3907 extend_virtual_window(&mut indices, 0, count, decoded.data.len(), 0);
3908 }
3909 drop(guard);
3910
3911 if let Some(reveal) = decoded.reveal_key.as_deref()
3912 && let Some(index) =
3913 (0..decoded.data.len()).find(|index| decoded.data.key(*index) == Some(reveal))
3914 {
3915 extend_virtual_window(
3916 &mut indices,
3917 index,
3918 count,
3919 decoded.data.len(),
3920 virtual_overdraw_items(decoded),
3921 );
3922 if let Some(header) = decoded.sticky_headers.range(..=index).next_back() {
3923 indices.insert(*header);
3924 }
3925 }
3926 if (decoded.bottom_align || decoded.follow_tail) && !decoded.data.is_empty() {
3927 let start = decoded.data.len().saturating_sub(count);
3928 indices.extend(start..decoded.data.len());
3929 }
3930 Ok(indices)
3931}
3932
3933fn same_collection_key_order(
3934 current: &crate::VirtualCollectionData,
3935 next: &crate::VirtualCollectionData,
3936) -> bool {
3937 current.len() == next.len()
3938 && (0..current.len()).all(|index| current.key(index) == next.key(index))
3939}
3940
3941fn virtual_overdraw_items(decoded: &DecodedVirtualCollection) -> usize {
3942 nonnegative_usize((decoded.overdraw_pixels / decoded.estimated_height).ceil())
3943}
3944
3945fn extend_virtual_window(
3946 indices: &mut BTreeSet<usize>,
3947 anchor: usize,
3948 count: usize,
3949 len: usize,
3950 leading: usize,
3951) {
3952 let start = anchor.saturating_sub(leading).min(len);
3953 let end = anchor.saturating_add(count).min(len);
3954 indices.extend(start..end);
3955}
3956
3957struct VirtualCollectionContext {
3958 component: ComponentInstancePath,
3959 context: UiContext,
3960 generation: ScriptGeneration,
3961}
3962
3963fn enter_virtual_collection_scope(
3964 active: &ActiveComponentRenderState,
3965 context: UiContext,
3966) -> Result<(), Box<EvalAltResult>> {
3967 let mut guard = active.try_borrow_mut().map_err(|_| {
3968 Box::new(component_render_error(
3969 "component render stack is already borrowed",
3970 ))
3971 })?;
3972 let render = guard.as_mut().ok_or_else(|| {
3973 Box::new(component_render_error(
3974 "virtual_collection may run only during view render",
3975 ))
3976 })?;
3977 render.stack.push(context.component_path().clone());
3978 render.contexts.push(context);
3979 render.effect_keys.push(None);
3980 Ok(())
3981}
3982
3983fn virtual_collection_context(
3984 active: &ActiveComponentRenderState,
3985) -> Result<VirtualCollectionContext, Box<EvalAltResult>> {
3986 let guard = active.try_borrow().map_err(|_| {
3987 Box::new(component_render_error(
3988 "component render stack is already borrowed",
3989 ))
3990 })?;
3991 let render = guard.as_ref().ok_or_else(|| {
3992 Box::new(component_render_error(
3993 "virtual_collection may run only during view render",
3994 ))
3995 })?;
3996 let context = render.contexts.last().cloned().ok_or_else(|| {
3997 Box::new(component_render_error(
3998 "component render context stack is empty",
3999 ))
4000 })?;
4001 Ok(VirtualCollectionContext {
4002 component: context.component_path().clone(),
4003 context: context.clone(),
4004 generation: render.generation,
4005 })
4006}
4007
4008fn collection_string(config: &mut Map, name: &str) -> Result<String, Box<EvalAltResult>> {
4009 config
4010 .remove(name)
4011 .and_then(Dynamic::try_cast::<ImmutableString>)
4012 .map(|value| value.to_string())
4013 .ok_or_else(|| {
4014 Box::new(component_render_error(format!(
4015 "virtual collection `{name}` must be a string"
4016 )))
4017 })
4018}
4019
4020fn collection_optional_string(
4021 config: &mut Map,
4022 name: &str,
4023) -> Result<Option<String>, Box<EvalAltResult>> {
4024 config
4025 .remove(name)
4026 .map(|value| {
4027 value
4028 .try_cast::<ImmutableString>()
4029 .map(|value| value.to_string())
4030 .ok_or_else(|| {
4031 Box::new(component_render_error(format!(
4032 "virtual collection `{name}` must be a string"
4033 )))
4034 })
4035 })
4036 .transpose()
4037}
4038
4039fn collection_optional_number(
4040 config: &mut Map,
4041 name: &str,
4042) -> Result<Option<f64>, Box<EvalAltResult>> {
4043 config
4044 .remove(name)
4045 .map(|value| {
4046 if value.is::<rhai::FLOAT>() {
4047 Ok(value.cast::<rhai::FLOAT>())
4048 } else if value.is::<rhai::INT>() {
4049 value
4050 .cast::<rhai::INT>()
4051 .to_string()
4052 .parse::<f64>()
4053 .map_err(|_| Box::new(component_render_error("invalid collection number")))
4054 } else {
4055 Err(Box::new(component_render_error(format!(
4056 "virtual collection `{name}` must be a number"
4057 ))))
4058 }
4059 })
4060 .transpose()
4061}
4062
4063fn collection_positive(config: &mut Map, name: &str) -> Result<f64, Box<EvalAltResult>> {
4064 let value = collection_optional_number(config, name)?.ok_or_else(|| {
4065 Box::new(component_render_error(format!(
4066 "virtual collection `{name}` is required"
4067 )))
4068 })?;
4069 if value.is_finite() && value > 0.0 {
4070 Ok(value)
4071 } else {
4072 Err(Box::new(component_render_error(format!(
4073 "virtual collection `{name}` must be finite and positive"
4074 ))))
4075 }
4076}
4077
4078fn collection_optional_bool(
4079 config: &mut Map,
4080 name: &str,
4081) -> Result<Option<bool>, Box<EvalAltResult>> {
4082 config
4083 .remove(name)
4084 .map(|value| {
4085 value.try_cast::<bool>().ok_or_else(|| {
4086 Box::new(component_render_error(format!(
4087 "virtual collection `{name}` must be bool"
4088 )))
4089 })
4090 })
4091 .transpose()
4092}
4093
4094fn collection_optional_indices(
4095 config: &mut Map,
4096 name: &str,
4097) -> Result<Option<BTreeSet<usize>>, Box<EvalAltResult>> {
4098 let Some(value) = config.remove(name) else {
4099 return Ok(None);
4100 };
4101 let values = value.try_cast::<Array>().ok_or_else(|| {
4102 Box::new(component_render_error(format!(
4103 "virtual collection `{name}` must be an array of non-negative integers"
4104 )))
4105 })?;
4106 let mut indices = BTreeSet::new();
4107 for (position, value) in values.into_iter().enumerate() {
4108 let index = value.try_cast::<rhai::INT>().ok_or_else(|| {
4109 Box::new(component_render_error(format!(
4110 "virtual collection `{name}` item {position} must be an integer"
4111 )))
4112 })?;
4113 let index = usize::try_from(index).map_err(|_| {
4114 Box::new(component_render_error(format!(
4115 "virtual collection `{name}` item {position} must be non-negative"
4116 )))
4117 })?;
4118 if !indices.insert(index) {
4119 return Err(Box::new(component_render_error(format!(
4120 "virtual collection `{name}` contains duplicate index {index}"
4121 ))));
4122 }
4123 }
4124 Ok(Some(indices))
4125}
4126
4127fn collection_data(config: &mut Map) -> Result<crate::VirtualCollectionData, Box<EvalAltResult>> {
4128 let data = config.remove("data").ok_or_else(|| {
4129 Box::new(component_render_error(
4130 "virtual collection `data` is required",
4131 ))
4132 })?;
4133 if data.is::<crate::NativeCollection>() {
4134 return Ok(crate::VirtualCollectionData::Native(
4135 data.cast::<crate::NativeCollection>(),
4136 ));
4137 }
4138 let data = data.try_cast::<Array>().ok_or_else(|| {
4139 Box::new(component_render_error(
4140 "virtual collection `data` must be an array or NativeCollection",
4141 ))
4142 })?;
4143 let data = data
4144 .into_iter()
4145 .map(|value| {
4146 UiValue::from_dynamic(value)
4147 .map_err(|error| Box::new(component_render_error(error.to_string())))
4148 })
4149 .collect::<Result<Vec<_>, _>>()?;
4150 let mut keys = BTreeSet::new();
4151 for (index, item) in data.iter().enumerate() {
4152 let (key, _) = collection_payload(item, index)?;
4153 if !keys.insert(key.clone()) {
4154 return Err(Box::new(component_render_error(format!(
4155 "virtual collection data key `{key}` is duplicated"
4156 ))));
4157 }
4158 }
4159 Ok(crate::VirtualCollectionData::Values(data))
4160}
4161
4162fn collection_payload(item: &UiValue, index: usize) -> Result<(String, Map), Box<EvalAltResult>> {
4163 let UiValue::Map(map) = item else {
4164 return Err(Box::new(component_render_error(
4165 "virtual collection data items must be maps with a string `key`",
4166 )));
4167 };
4168 let key = match map.get("key") {
4169 Some(UiValue::String(key)) if !key.is_empty() => key.clone(),
4170 _ => {
4171 return Err(Box::new(component_render_error(
4172 "virtual collection data item `key` must be a non-empty string",
4173 )));
4174 }
4175 };
4176 Ok((
4177 key.clone(),
4178 Map::from_iter([
4179 ("key".into(), Dynamic::from(key)),
4180 (
4181 "index".into(),
4182 Dynamic::from_int(rhai::INT::try_from(index).unwrap_or(rhai::INT::MAX)),
4183 ),
4184 ("item".into(), item.clone().into_dynamic()),
4185 ]),
4186 ))
4187}
4188
4189fn add_collection_neighbors(data: &crate::VirtualCollectionData, index: usize, payload: &mut Map) {
4190 let key = |index: Option<usize>| {
4191 index
4192 .and_then(|index| data.key(index))
4193 .map_or(Dynamic::UNIT, |key| Dynamic::from(key.to_owned()))
4194 };
4195 payload.insert("previous_key".into(), key(index.checked_sub(1)));
4196 payload.insert(
4197 "next_key".into(),
4198 key(index.checked_add(1).filter(|index| *index < data.len())),
4199 );
4200 payload.insert("first_key".into(), key((!data.is_empty()).then_some(0)));
4201 payload.insert("last_key".into(), key(data.len().checked_sub(1)));
4202}
4203
4204fn nonnegative_usize(value: f64) -> usize {
4205 value.to_string().parse().unwrap_or(usize::MAX)
4206}
4207
4208fn component_event_callbacks(
4209 component: &crate::ComponentDefinition,
4210 props: &Map,
4211) -> Vec<(String, FnPtr)> {
4212 component
4213 .schema
4214 .events
4215 .keys()
4216 .filter_map(|event| {
4217 props
4218 .get(format!("on_{event}").as_str())
4219 .filter(|value| value.is::<FnPtr>())
4220 .map(|value| (event.clone(), value.clone_cast::<FnPtr>()))
4221 })
4222 .collect()
4223}
4224
4225fn bind_component_callback_props(
4226 props: &mut Map,
4227 schema: &BTreeMap<String, crate::ObjectField>,
4228 binding: &ComponentCallbackBinding,
4229) {
4230 for (name, field) in schema {
4231 if schema_contains_callback(&field.schema)
4232 && let Some(value) = props.get_mut(name.as_str())
4233 {
4234 bind_component_callback_value(&field.schema, value, binding);
4235 }
4236 }
4237}
4238
4239fn schema_contains_callback(schema: &crate::ValueSchema) -> bool {
4240 match schema {
4241 crate::ValueSchema::Callback => true,
4242 crate::ValueSchema::Array { items, .. }
4243 | crate::ValueSchema::Map { values: items }
4244 | crate::ValueSchema::Optional { value: items } => schema_contains_callback(items),
4245 crate::ValueSchema::Object { fields, .. } => fields
4246 .values()
4247 .any(|field| schema_contains_callback(&field.schema)),
4248 crate::ValueSchema::OneOf { variants } => variants.iter().any(schema_contains_callback),
4249 _ => false,
4250 }
4251}
4252
4253fn bind_component_callback_value(
4254 schema: &crate::ValueSchema,
4255 value: &mut Dynamic,
4256 binding: &ComponentCallbackBinding,
4257) {
4258 match schema {
4259 crate::ValueSchema::Callback if value.is::<FnPtr>() => {
4260 let mut function = value.clone_cast::<FnPtr>();
4261 if !function
4262 .iter_curry()
4263 .any(Dynamic::is::<ComponentCallbackBinding>)
4264 {
4265 function.add_curry(Dynamic::from(binding.clone()));
4266 *value = Dynamic::from(function);
4267 }
4268 }
4269 crate::ValueSchema::Optional { value: inner } if !value.is_unit() => {
4270 bind_component_callback_value(inner, value, binding);
4271 }
4272 crate::ValueSchema::OneOf { variants } => {
4273 if let Some(variant) = variants
4274 .iter()
4275 .find(|variant| variant.validate(value).is_ok())
4276 {
4277 bind_component_callback_value(variant, value, binding);
4278 }
4279 }
4280 crate::ValueSchema::Array { items, .. }
4281 if schema_contains_callback(items) && value.is::<Array>() =>
4282 {
4283 let mut values = value.clone_cast::<Array>();
4284 for value in &mut values {
4285 bind_component_callback_value(items, value, binding);
4286 }
4287 *value = Dynamic::from_array(values);
4288 }
4289 crate::ValueSchema::Map {
4290 values: item_schema,
4291 } if schema_contains_callback(item_schema) && value.is::<Map>() => {
4292 let mut values = value.clone_cast::<Map>();
4293 for value in values.values_mut() {
4294 bind_component_callback_value(item_schema, value, binding);
4295 }
4296 *value = Dynamic::from_map(values);
4297 }
4298 crate::ValueSchema::Object { fields, .. } if value.is::<Map>() => {
4299 let mut values = value.clone_cast::<Map>();
4300 for (name, field) in fields {
4301 if schema_contains_callback(&field.schema)
4302 && let Some(value) = values.get_mut(name.as_str())
4303 {
4304 bind_component_callback_value(&field.schema, value, binding);
4305 }
4306 }
4307 *value = Dynamic::from_map(values);
4308 }
4309 _ => {}
4310 }
4311}
4312
4313fn bind_component_node_props(
4314 props: &mut Map,
4315 schema: &BTreeMap<String, crate::ObjectField>,
4316 binding: &ComponentCallbackBinding,
4317) {
4318 for (name, field) in schema {
4319 if schema_contains_node(&field.schema)
4320 && let Some(value) = props.get_mut(name.as_str())
4321 {
4322 bind_component_node_value(&field.schema, value, binding);
4323 }
4324 }
4325}
4326
4327fn schema_contains_node(schema: &crate::ValueSchema) -> bool {
4328 match schema {
4329 crate::ValueSchema::Node => true,
4330 crate::ValueSchema::Array { items, .. }
4331 | crate::ValueSchema::Map { values: items }
4332 | crate::ValueSchema::Optional { value: items } => schema_contains_node(items),
4333 crate::ValueSchema::Object { fields, .. } => fields
4334 .values()
4335 .any(|field| schema_contains_node(&field.schema)),
4336 crate::ValueSchema::OneOf { variants } => variants.iter().any(schema_contains_node),
4337 _ => false,
4338 }
4339}
4340
4341fn bind_component_node_value(
4342 schema: &crate::ValueSchema,
4343 value: &mut Dynamic,
4344 binding: &ComponentCallbackBinding,
4345) {
4346 match schema {
4347 crate::ValueSchema::Node if value.is::<UiNode>() => {
4348 let mut node = value.clone_cast::<UiNode>();
4349 node.bind_component_scope(
4350 &binding.component,
4351 binding.incarnation,
4352 &binding.events,
4353 binding.context.as_ref(),
4354 );
4355 node.activate_component_snapshots();
4356 *value = Dynamic::from(node);
4357 }
4358 crate::ValueSchema::Optional { value: inner } if !value.is_unit() => {
4359 bind_component_node_value(inner, value, binding);
4360 }
4361 crate::ValueSchema::OneOf { variants } => {
4362 if let Some(variant) = variants
4363 .iter()
4364 .find(|variant| variant.validate(value).is_ok())
4365 {
4366 bind_component_node_value(variant, value, binding);
4367 }
4368 }
4369 crate::ValueSchema::Array { items, .. }
4370 if schema_contains_node(items) && value.is::<Array>() =>
4371 {
4372 let mut values = value.clone_cast::<Array>();
4373 for value in &mut values {
4374 bind_component_node_value(items, value, binding);
4375 }
4376 *value = Dynamic::from_array(values);
4377 }
4378 crate::ValueSchema::Map {
4379 values: item_schema,
4380 } if schema_contains_node(item_schema) && value.is::<Map>() => {
4381 let mut values = value.clone_cast::<Map>();
4382 for value in values.values_mut() {
4383 bind_component_node_value(item_schema, value, binding);
4384 }
4385 *value = Dynamic::from_map(values);
4386 }
4387 crate::ValueSchema::Object { fields, .. } if value.is::<Map>() => {
4388 let mut values = value.clone_cast::<Map>();
4389 for (name, field) in fields {
4390 if schema_contains_node(&field.schema)
4391 && let Some(value) = values.get_mut(name.as_str())
4392 {
4393 bind_component_node_value(&field.schema, value, binding);
4394 }
4395 }
4396 *value = Dynamic::from_map(values);
4397 }
4398 _ => {}
4399 }
4400}
4401
4402fn hydrate_replayed_node_value(value: &mut Dynamic) {
4403 if value.is::<UiNode>() {
4404 let mut node = value.clone_cast::<UiNode>();
4405 node.hydrate_component_subtrees();
4406 *value = Dynamic::from(node);
4407 } else if value.is::<Array>() {
4408 let mut values = value.clone_cast::<Array>();
4409 for value in &mut values {
4410 hydrate_replayed_node_value(value);
4411 }
4412 *value = Dynamic::from_array(values);
4413 } else if value.is::<Map>() {
4414 let mut values = value.clone_cast::<Map>();
4415 for value in values.values_mut() {
4416 hydrate_replayed_node_value(value);
4417 }
4418 *value = Dynamic::from_map(values);
4419 }
4420}
4421
4422fn component_part_styles(props: &Map) -> BTreeMap<String, crate::Style> {
4423 props
4424 .get("part_styles")
4425 .filter(|value| value.is::<Map>())
4426 .map(Dynamic::clone_cast::<Map>)
4427 .unwrap_or_default()
4428 .into_iter()
4429 .filter_map(|(name, value)| {
4430 value
4431 .is::<crate::Style>()
4432 .then(|| (name.to_string(), value.cast::<crate::Style>()))
4433 })
4434 .collect()
4435}
4436
4437fn component_root_style(props: &Map) -> Option<crate::Style> {
4438 props
4439 .get("style")
4440 .filter(|value| value.is::<crate::Style>())
4441 .map(Dynamic::clone_cast::<crate::Style>)
4442}
4443
4444fn register_component_event_callbacks(
4445 active: &ActiveComponentRenderState,
4446 component: &ComponentInstancePath,
4447 caller: &UiContext,
4448 callbacks: Vec<(String, FnPtr)>,
4449 native_context: &crate::invocation::ScriptInvocationContext,
4450) -> Result<(), Box<EvalAltResult>> {
4451 let mut guard = active.try_borrow_mut().map_err(|_| {
4452 Box::new(component_render_error(
4453 "component render stack is already borrowed",
4454 ))
4455 })?;
4456 let active = guard.as_mut().ok_or_else(|| {
4457 Box::new(component_render_error(
4458 "formal component rendering may run only inside view",
4459 ))
4460 })?;
4461 for (event, function) in callbacks {
4462 let mut callback = ScriptCallback::try_from_fn_ptr(function, active.generation)
4463 .map_err(|error| Box::new(component_render_error(error.to_string())))?;
4464 callback.bind_component_scope_if_unset(
4465 caller.component_path(),
4466 caller.component_incarnation(),
4467 caller.event_schemas().clone(),
4468 );
4469 callback.bind_native_context_if_unset(native_context.clone());
4470 active
4471 .event_handlers
4472 .insert((component.clone(), event), callback);
4473 }
4474 Ok(())
4475}
4476
4477fn component_render_error(message: impl Into<String>) -> EvalAltResult {
4478 EvalAltResult::ErrorRuntime(Dynamic::from(message.into()), Position::NONE)
4479}
4480
4481#[cfg(feature = "charts")]
4482fn register_chart_validation_api(engine: &mut Engine) {
4483 engine.register_fn(
4484 "chart_validate",
4485 |spec: Map, data: Dynamic, key_dimension: Dynamic| -> Result<(), Box<EvalAltResult>> {
4486 let spec = UiValue::from_dynamic(Dynamic::from_map(spec))
4487 .map_err(|error| Box::new(component_render_error(error.to_string())))?;
4488 crate::ChartSpec::from_ui_value(&spec)
4489 .map_err(|error| Box::new(component_render_error(error.to_string())))?;
4490 let key_dimension = if key_dimension.is_unit() {
4491 None
4492 } else if key_dimension.is::<ImmutableString>() {
4493 Some(key_dimension.cast::<ImmutableString>().to_string())
4494 } else {
4495 return Err(Box::new(component_render_error(
4496 "chart key_dimension must be a string or null",
4497 )));
4498 };
4499 if data.is::<crate::NativeChartData>() {
4500 return Ok(());
4501 }
4502 if !data.is::<Array>() {
4503 return Err(Box::new(component_render_error(
4504 "chart data must be an array of objects or NativeChartData",
4505 )));
4506 }
4507 let data = UiValue::from_dynamic(Dynamic::from_array(data.cast::<Array>()))
4508 .map_err(|error| Box::new(component_render_error(error.to_string())))?;
4509 let UiValue::Array(rows) = data else {
4510 unreachable!()
4511 };
4512 if rows.len() > 10_000 {
4513 return Err(Box::new(component_render_error(
4514 "inline chart data is limited to 10000 rows; use NativeChartData",
4515 )));
4516 }
4517 let rows = rows
4518 .into_iter()
4519 .enumerate()
4520 .map(|(index, row)| match row {
4521 UiValue::Map(row) => Ok(row),
4522 _ => Err(Box::new(component_render_error(format!(
4523 "chart inline row {index} must be an object"
4524 )))),
4525 })
4526 .collect::<Result<Vec<_>, _>>()?;
4527 crate::ChartDataset::from_rows(
4528 "main",
4529 &rows,
4530 key_dimension,
4531 crate::ChartDataLimits::default(),
4532 )
4533 .map_err(|error| Box::new(component_render_error(error.to_string())))?;
4534 Ok(())
4535 },
4536 );
4537}
4538
4539#[cfg(test)]
4540mod tests {
4541 use super::*;
4542
4543 fn virtual_config(sticky: &[i64], alignment: &str) -> Map {
4544 let mut item = Map::new();
4545 item.insert("key".into(), Dynamic::from("header"));
4546 let mut config = Map::new();
4547 config.insert("key".into(), Dynamic::from("sections"));
4548 config.insert(
4549 "data".into(),
4550 Dynamic::from_array(vec![Dynamic::from_map(item)]),
4551 );
4552 config.insert("estimated_height".into(), Dynamic::from_float(30.0));
4553 config.insert("height".into(), Dynamic::from_float(120.0));
4554 config.insert("alignment".into(), Dynamic::from(alignment.to_owned()));
4555 config.insert(
4556 "sticky_headers".into(),
4557 Dynamic::from_array(sticky.iter().copied().map(Dynamic::from_int).collect()),
4558 );
4559 config
4560 }
4561
4562 #[test]
4563 fn sticky_virtual_collection_indices_are_bounded_unique_and_top_aligned() {
4564 let decoded = decode_virtual_collection(virtual_config(&[0], "top")).unwrap();
4565 assert_eq!(decoded.sticky_headers.as_ref(), &BTreeSet::from([0]));
4566
4567 for (sticky, alignment, expected) in [
4568 (&[1][..], "top", "outside 1 items"),
4569 (&[0, 0][..], "top", "duplicate index 0"),
4570 (&[0][..], "bottom", "require top-aligned"),
4571 ] {
4572 let error = decode_virtual_collection(virtual_config(sticky, alignment)).unwrap_err();
4573 assert!(error.to_string().contains(expected), "{error}");
4574 }
4575 }
4576
4577 #[test]
4578 fn virtual_collection_reveal_key_is_known_and_has_one_scroll_owner() {
4579 let mut config = virtual_config(&[], "top");
4580 config.insert("reveal_key".into(), Dynamic::from("header"));
4581 let decoded = decode_virtual_collection(config).unwrap();
4582 assert_eq!(decoded.reveal_key.as_deref(), Some("header"));
4583
4584 let mut unknown = virtual_config(&[], "top");
4585 unknown.insert("reveal_key".into(), Dynamic::from("missing"));
4586 let error = decode_virtual_collection(unknown).unwrap_err();
4587 assert!(error.to_string().contains("is not present"), "{error}");
4588
4589 let mut competing = virtual_config(&[], "top");
4590 competing.insert("reveal_key".into(), Dynamic::from("header"));
4591 competing.insert("follow_tail".into(), Dynamic::from(true));
4592 let error = decode_virtual_collection(competing).unwrap_err();
4593 assert!(error.to_string().contains("must not combine"), "{error}");
4594 }
4595
4596 #[test]
4597 fn script_builds_a_declarative_tree() {
4598 let mut runtime = RuntimeEngine::new();
4599 let compiled = runtime
4600 .compile(
4601 r#"
4602 fn view() {
4603 column([
4604 text("Hello").with_key("greeting"),
4605 text("from Rhai")
4606 ])
4607 }
4608 "#,
4609 )
4610 .expect("script should compile");
4611
4612 let root = runtime.render(&compiled).expect("view should render");
4613 assert_eq!(
4614 root.source().map(|source| source.module.as_str()),
4615 Some("ui/main.rhai")
4616 );
4617 let crate::UiNodeKind::Box { children } = root.kind() else {
4618 panic!("column must render a box");
4619 };
4620 assert_eq!(children.len(), 2);
4621 assert_eq!(
4622 children[0].key().map(crate::NodeKey::as_str),
4623 Some("greeting")
4624 );
4625 assert!(children.iter().all(|child| child.source().is_some()));
4626 }
4627
4628 #[test]
4629 fn runtime_emits_official_rhai_language_server_definitions() {
4630 let runtime = RuntimeEngine::new();
4631 let definitions = runtime.definition_source();
4632 assert!(definitions.starts_with("module static;"));
4633 assert!(definitions.contains("fn box"));
4634 assert!(definitions.contains("fn timeout"));
4635 assert!(definitions.contains("fn canvas_fill_path"));
4636 assert!(definitions.contains("UiNode"));
4637 }
4638
4639 #[test]
4640 fn retained_callbacks_reject_capturing_functions_and_non_data_curry() {
4641 let mut runtime = RuntimeEngine::new();
4642 let compiled = runtime
4643 .compile(r#"fn view() { text("x").on_click(|| ()) }"#)
4644 .unwrap();
4645 let error = runtime.render(&compiled).unwrap_err();
4646 assert!(
4647 error
4648 .to_string()
4649 .contains("anonymous or capturing functions")
4650 );
4651
4652 let mut callback = FnPtr::new("clicked").unwrap();
4653 callback.add_curry(Dynamic::from(UiNode::text("not durable data")));
4654 assert!(matches!(
4655 ScriptCallback::try_from_fn_ptr(callback, ScriptGeneration::initial()),
4656 Err(ScriptCallbackDefinitionError::InvalidCurry { index: 0, .. })
4657 ));
4658 }
4659
4660 #[test]
4661 fn replay_hydrates_formal_subtrees_inside_nested_dynamic_node_shapes() {
4662 let root = ComponentInstancePath::root("View", "main");
4663 let outer = root.child("Outer", "outer");
4664 let inner = root.child("Inner", "inner");
4665 let inner_snapshot = crate::node::ComponentOwnedSnapshot::default();
4666 let mut current_inner = UiNode::text("current")
4667 .with_component_root_snapshot(inner.clone(), inner_snapshot.reference());
4668 current_inner.activate_component_snapshots();
4669 let outer_snapshot = crate::node::ComponentOwnedSnapshot::default();
4670 let mut current_outer = UiNode::box_node(vec![
4671 UiNode::text("outer"),
4672 UiNode::text("stale-inner")
4673 .with_component_root_snapshot(inner, inner_snapshot.reference()),
4674 ])
4675 .with_component_root_snapshot(outer.clone(), outer_snapshot.reference());
4676 current_outer.activate_component_snapshots();
4677 let stale = UiNode::box_node(vec![
4678 UiNode::text("stale-outer")
4679 .with_component_root_snapshot(outer, outer_snapshot.reference())
4680 .with_attribute("presentation", UiValue::Bool(true)),
4681 ]);
4682 let mut nested = Map::new();
4683 nested.insert(
4684 "object".into(),
4685 Dynamic::from_map(Map::from_iter([(
4686 "nodes".into(),
4687 Dynamic::from_array(vec![Dynamic::from(stale)]),
4688 )])),
4689 );
4690 let mut value = Dynamic::from_map(nested);
4691
4692 hydrate_replayed_node_value(&mut value);
4693
4694 let nested = value.cast::<Map>();
4695 let object = nested["object"].clone_cast::<Map>();
4696 let nodes = object["nodes"].clone_cast::<Array>();
4697 let node = nodes[0].clone_cast::<UiNode>();
4698 let crate::UiNodeKind::Box { children } = node.kind() else {
4699 panic!("nested node shape must preserve its raw wrapper");
4700 };
4701 assert_eq!(
4702 children[0].attributes().get("presentation"),
4703 Some(&UiValue::Bool(true))
4704 );
4705 let crate::UiNodeKind::Box { children } = children[0].kind() else {
4706 panic!("outer component must hydrate its owned snapshot");
4707 };
4708 assert!(
4709 matches!(children[1].kind(), crate::UiNodeKind::Text { text } if text == "current")
4710 );
4711 }
4712
4713 #[test]
4714 fn final_component_api_registers_render_once_and_invokes_by_id() {
4715 let mut runtime = RuntimeEngine::new();
4716 let compiled = runtime
4717 .compile(
4718 r#"
4719 define_component(#{
4720 metadata: #{
4721 id: "components/message", "export": "Message", version: "0.1.0",
4722 runtime_api: #{ min_inclusive: 3, max_exclusive: 4 },
4723 dependencies: [], capabilities: #{}
4724 },
4725 schema: #{ props: #{}, state: #{ fields: #{} }, events: #{}, slots: #{}, parts: [] },
4726 render: Fn("render_Message")
4727 });
4728 fn Message(props) { render_component("components/message", props) }
4729 fn render_Message(ctx, props) { text("final component api") }
4730 fn view() { Message(#{ key: "message" }) }
4731 "#,
4732 )
4733 .unwrap();
4734 let root = runtime.render(&compiled).unwrap();
4735 assert!(matches!(
4736 root.kind(),
4737 crate::UiNodeKind::Text { text } if text == "final component api"
4738 ));
4739 let recipe = runtime.component_invocations().next().unwrap();
4740 assert_eq!(recipe.render_name(), "render_Message");
4741 }
4742
4743 #[test]
4744 fn self_contained_compile_rejects_dynamic_imports_before_evaluation() {
4745 let mut runtime = RuntimeEngine::new();
4746 assert!(matches!(
4747 runtime.compile_self_contained_named(
4748 "ui/dynamic_import.rhai",
4749 "let path = \"components/button\"; import path as button;",
4750 ),
4751 Err(RuntimeError::Import(_))
4752 ));
4753 }
4754
4755 #[test]
4756 fn script_builds_an_error_boundary() {
4757 let mut runtime = RuntimeEngine::new();
4758 let compiled = runtime
4759 .compile(
4760 r#"
4761 fn view() {
4762 error_boundary(text("content"), text("fallback"))
4763 }
4764 "#,
4765 )
4766 .unwrap();
4767 let root = runtime.render(&compiled).unwrap();
4768 assert!(matches!(
4769 root.kind(),
4770 crate::UiNodeKind::ErrorBoundary { .. }
4771 ));
4772 }
4773
4774 #[test]
4775 fn lazy_error_boundary_catches_rhai_evaluation_failure() {
4776 let mut runtime = RuntimeEngine::new();
4777 let compiled = runtime
4778 .compile(
4779 r#"
4780 fn broken() { throw "boom"; }
4781 fn fallback() { text("fallback") }
4782 fn view() { error_boundary_lazy(Fn("broken"), Fn("fallback")) }
4783 "#,
4784 )
4785 .unwrap();
4786 let root = runtime.render(&compiled).unwrap();
4787 assert!(matches!(root.kind(), crate::UiNodeKind::Text { text } if text == "fallback"));
4788 assert!(matches!(
4789 root.attributes().get("boundary_error"),
4790 Some(crate::UiValue::String(message)) if message.contains("boom")
4791 ));
4792 }
4793
4794 #[test]
4795 fn script_builds_a_typed_controlled_overlay() {
4796 let mut runtime = RuntimeEngine::new();
4797 let compiled = runtime
4798 .compile(
4799 r#"
4800 fn changed(ctx, open) { () }
4801 fn view() {
4802 overlay(
4803 text("trigger"),
4804 text("content"),
4805 #{
4806 id: "account",
4807 kind: "popover",
4808 placement: "bottom",
4809 open: true,
4810 gap: 6
4811 }
4812 ).on_open_change(Fn("changed"))
4813 }
4814 "#,
4815 )
4816 .unwrap();
4817 let root = runtime.render(&compiled).unwrap();
4818 assert!(matches!(
4819 root.kind(),
4820 crate::UiNodeKind::Overlay { spec, .. }
4821 if spec.id.as_str() == "account"
4822 && spec.open
4823 && (spec.gap - 6.0).abs() < f64::EPSILON
4824 && spec.placement == crate::OverlayPlacement::Bottom
4825 ));
4826 assert_eq!(
4827 root.handler("open_change")
4828 .unwrap()
4829 .as_script()
4830 .unwrap()
4831 .generation(),
4832 compiled.generation()
4833 );
4834 }
4835
4836 #[test]
4837 fn click_handlers_can_carry_typed_semantic_payloads() {
4838 let mut runtime = RuntimeEngine::new();
4839 let compiled = runtime
4840 .compile(
4841 r#"
4842 fn changed(ctx, value) { () }
4843 fn view() {
4844 text("toggle").on_click_value(
4845 Fn("changed"), #{ checked: true, source: "pointer" }
4846 )
4847 }
4848 "#,
4849 )
4850 .unwrap();
4851 let root = runtime.render(&compiled).unwrap();
4852 assert_eq!(
4853 root.event_handlers("click")[0].value(),
4854 Some(&crate::UiValue::Map(BTreeMap::from([
4855 ("checked".to_owned(), crate::UiValue::Bool(true)),
4856 (
4857 "source".to_owned(),
4858 crate::UiValue::String("pointer".to_owned()),
4859 ),
4860 ])))
4861 );
4862 assert!(root.handlers().contains_key("click"));
4863 }
4864
4865 #[test]
4866 fn rhai_nodes_retain_ordered_handlers_for_all_event_phases() {
4867 let mut runtime = RuntimeEngine::new();
4868 let compiled = runtime
4869 .compile(
4870 r#"
4871 fn first(ctx, event) { event_response().prevent_default() }
4872 fn second(ctx, event) { event_response().stop_immediate() }
4873 fn capture(ctx, event) { propagate() }
4874 fn view() {
4875 text("drag")
4876 .on_capture("pointer_down", Fn("capture"))
4877 .on("pointer_down", Fn("first"))
4878 .on("pointer_down", Fn("second"))
4879 }
4880 "#,
4881 )
4882 .unwrap();
4883 let root = runtime.render(&compiled).unwrap();
4884 let bindings = root.event_handlers("pointer_down");
4885 assert_eq!(bindings.len(), 3);
4886 assert_eq!(bindings[0].phase(), crate::EventPhase::Capture);
4887 assert_eq!(bindings[1].phase(), crate::EventPhase::Target);
4888 assert_eq!(bindings[2].phase(), crate::EventPhase::Target);
4889 assert_eq!(bindings[2].handler().as_script().unwrap().name(), "second");
4890 }
4891
4892 #[test]
4893 fn rhai_nodes_attach_only_registered_schema_checked_native_handlers() {
4894 let mut runtime = RuntimeEngine::new();
4895 let id = crate::NativeHandlerId::parse("timeline.drag").unwrap();
4896 runtime
4897 .register_native_handler(
4898 crate::NativeHandlerDescriptor::new(
4899 id,
4900 BTreeMap::from([("pointer_down".to_owned(), crate::ValueSchema::UiValue)]),
4901 )
4902 .unwrap(),
4903 |_, _, _, _| Ok(crate::EventResponse::new().capture_pointer()),
4904 )
4905 .unwrap();
4906 let compiled = runtime
4907 .compile(
4908 r#"
4909 fn view() {
4910 text("drag").on(
4911 "pointer_down", native_handler("timeline.drag")
4912 )
4913 }
4914 "#,
4915 )
4916 .unwrap();
4917 let root = runtime.render(&compiled).unwrap();
4918 assert!(matches!(
4919 root.handler("pointer_down"),
4920 Some(crate::UiEventHandler::Native(reference))
4921 if reference.descriptor().id.as_str() == "timeline.drag"
4922 ));
4923
4924 let input = runtime
4925 .compile(
4926 r#"
4927 fn view() {
4928 gpui_rhai::TextInputPrimitive(#{
4929 key: "native-input", value: "", placeholder: "Edit",
4930 disabled: false, read_only: false, typography: "body",
4931 on_change: native_handler("timeline.drag")
4932 })
4933 }
4934 "#,
4935 )
4936 .unwrap();
4937 assert!(
4938 runtime.render(&input).is_ok(),
4939 "native handlers must cross formal component and primitive callback props"
4940 );
4941
4942 let missing = runtime
4943 .compile(
4944 "fn view() { text(\"x\").on(\"pointer_down\", native_handler(\"app.missing\")) }",
4945 )
4946 .unwrap();
4947 assert!(runtime.render(&missing).is_err());
4948 }
4949
4950 #[test]
4951 fn final_box_and_fragment_atoms_are_distinct_snapshots() {
4952 let mut runtime = RuntimeEngine::new();
4953 let compiled = runtime
4954 .compile(
4955 r#"
4956 fn view() {
4957 box([text("a"), fragment([text("b"), text("c")])])
4958 }
4959 "#,
4960 )
4961 .unwrap();
4962 let root = runtime.render(&compiled).unwrap();
4963 let crate::UiNodeKind::Box { children } = root.kind() else {
4964 panic!("root must be the final Box atom");
4965 };
4966 assert!(matches!(
4967 children[1].kind(),
4968 crate::UiNodeKind::Fragment { children } if children.len() == 2
4969 ));
4970 }
4971
4972 #[test]
4973 fn text_accepts_typed_inline_span_runs() {
4974 let mut runtime = RuntimeEngine::new();
4975 let compiled = runtime
4976 .compile(
4977 r#"
4978 fn view() {
4979 text([
4980 span("Hello ").bold(),
4981 span("world").color(rgb(0x22cc88)).italic()
4982 ])
4983 }
4984 "#,
4985 )
4986 .unwrap();
4987 let root = runtime.render(&compiled).unwrap();
4988 let crate::UiNodeKind::RichText { text, spans } = root.kind() else {
4989 panic!("typed spans must produce rich text");
4990 };
4991 assert_eq!(text.as_str(), "Hello world");
4992 assert_eq!(spans.len(), 2);
4993 assert!(spans[0].is_bold());
4994 assert!(spans[1].is_italic());
4995 }
4996
4997 #[test]
4998 fn canvas_scene_is_keyed_validated_and_script_constructible() {
4999 let mut runtime = RuntimeEngine::new();
5000 let compiled = runtime
5001 .compile(
5002 r#"
5003 fn view() {
5004 canvas(canvas_scene([
5005 canvas_rect("panel", 0.0, 0.0, 100.0, 40.0, rgb(0x112233)),
5006 canvas_circle("dot", 20.0, 20.0, 8.0, rgb(0xffcc00)),
5007 canvas_line("axis", 0.0, 39.0, 100.0, 39.0, 1.0, rgb(0xffffff))
5008 ]))
5009 }
5010 "#,
5011 )
5012 .unwrap();
5013 let root = runtime.render(&compiled).unwrap();
5014 assert!(matches!(
5015 root.kind(),
5016 crate::UiNodeKind::Canvas { scene } if scene.commands().len() == 3
5017 ));
5018 }
5019
5020 #[test]
5021 fn inline_svg_atom_is_bounded_and_self_contained() {
5022 let mut runtime = RuntimeEngine::new();
5023 let compiled = runtime
5024 .compile(
5025 r#"
5026 fn view() {
5027 svg("<svg viewBox='0 0 10 10'><path fill='currentColor' d='M0 0L10 10'/></svg>")
5028 }
5029 "#,
5030 )
5031 .unwrap();
5032 let root = runtime.render(&compiled).unwrap();
5033 assert!(matches!(
5034 root.kind(),
5035 crate::UiNodeKind::Svg { source } if source.as_str().contains("currentColor")
5036 ));
5037
5038 let rejected = runtime
5039 .compile(r#"fn view() { svg("<svg><image href='https://example.com/a.png'/></svg>") }"#)
5040 .unwrap();
5041 assert!(runtime.render(&rejected).is_err());
5042 }
5043
5044 #[test]
5045 fn recompilation_invalidates_previous_generation() {
5046 let mut runtime = RuntimeEngine::new();
5047 let first = runtime.compile("fn view() { text(\"first\") }").unwrap();
5048 runtime.render(&first).unwrap();
5049 let second = runtime.compile("fn view() { text(\"second\") }").unwrap();
5050
5051 assert!(runtime.is_current(first.generation()));
5052 assert!(!runtime.is_current(second.generation()));
5053 runtime.render(&second).unwrap();
5054 assert!(!runtime.is_current(first.generation()));
5055 assert!(runtime.is_current(second.generation()));
5056 }
5057
5058 #[test]
5059 fn callbacks_are_rejected_after_reload() {
5060 let mut runtime = RuntimeEngine::new();
5061 let first = runtime
5062 .compile(
5063 r#"
5064 fn view() { text("first") }
5065 fn add_suffix(value) { value + "!" }
5066 "#,
5067 )
5068 .unwrap();
5069 runtime.render(&first).unwrap();
5070 let callback = runtime.callback(&first, "add_suffix").unwrap();
5071
5072 let value = runtime
5073 .invoke_callback(&first, &callback, ("hello",))
5074 .unwrap();
5075 assert_eq!(value.into_immutable_string().unwrap(), "hello!");
5076
5077 let second = runtime.compile("fn view() { text(\"second\") }").unwrap();
5078 runtime.render(&second).unwrap();
5079 assert!(matches!(
5080 runtime.invoke_callback(&second, &callback, ("hello",)),
5081 Err(RuntimeError::StaleCallback { .. })
5082 ));
5083 }
5084
5085 #[test]
5086 fn failed_candidate_keeps_last_good_generation_active() {
5087 let mut runtime = RuntimeEngine::new();
5088 let active = runtime
5089 .compile(
5090 r#"
5091 fn view() { text("active") }
5092 fn action(value) { value }
5093 "#,
5094 )
5095 .unwrap();
5096 runtime.render(&active).unwrap();
5097 let callback = runtime.callback(&active, "action").unwrap();
5098
5099 let candidate = runtime
5100 .compile("fn view() { throw \"render failed\"; }")
5101 .unwrap();
5102 assert!(runtime.render(&candidate).is_err());
5103 assert!(runtime.is_current(active.generation()));
5104 let _ = runtime
5105 .invoke_callback(&active, &callback, ("still active",))
5106 .unwrap();
5107 }
5108
5109 #[test]
5110 fn failed_nested_component_records_the_deepest_component_path() {
5111 let mut runtime = RuntimeEngine::new();
5112 let compiled = runtime
5113 .compile(
5114 r#"
5115 define_component(#{
5116 metadata: #{ id: "components/broken", "export": "Broken",
5117 version: "0.1.3",
5118 runtime_api: #{ min_inclusive: 3, max_exclusive: 4 },
5119 dependencies: [], capabilities: #{} },
5120 schema: #{ props: #{}, state: #{ fields: #{} }, events: #{},
5121 slots: #{}, parts: [] },
5122 render: Fn("render_Broken")
5123 });
5124 fn Broken(props) { render_component("components/broken", props) }
5125 fn render_Broken(ctx, props) { throw "nested failure"; }
5126 fn view() { Broken(#{ key: "inner" }) }
5127 "#,
5128 )
5129 .unwrap();
5130
5131 assert!(runtime.render(&compiled).is_err());
5132 assert_eq!(
5133 runtime
5134 .last_failed_component()
5135 .as_ref()
5136 .map(ToString::to_string),
5137 Some("/App[root]/Broken[inner]".to_owned())
5138 );
5139 assert!(runtime.last_failed_timing().is_some());
5140
5141 let recovered = runtime
5142 .compile("fn view() { text(\"recovered\") }")
5143 .unwrap();
5144 runtime.render(&recovered).unwrap();
5145 assert_eq!(runtime.last_failed_component(), None);
5146 assert_eq!(runtime.last_failed_timing(), None);
5147 }
5148
5149 #[test]
5150 fn execution_timings_cover_compile_and_render() {
5151 let mut runtime = RuntimeEngine::new();
5152 runtime.set_slow_threshold(Duration::ZERO);
5153 let compiled = runtime
5154 .compile(
5155 r#"
5156 fn view() { text("timed") }
5157 fn counted() { let total = 0; for value in 0..100 { total += value; } total }
5158 "#,
5159 )
5160 .unwrap();
5161 runtime.render(&compiled).unwrap();
5162 let callback = runtime.callback(&compiled, "counted").unwrap();
5163 let _ = runtime.invoke_callback(&compiled, &callback, ()).unwrap();
5164 let timings = runtime.take_timings();
5165 assert_eq!(timings.len(), 3);
5166 assert_eq!(timings[0].operation, ExecutionOperation::Compile);
5167 assert_eq!(timings[1].operation, ExecutionOperation::Render);
5168 assert_eq!(
5169 timings[2].operation,
5170 ExecutionOperation::Callback("counted".to_owned())
5171 );
5172 assert_eq!(timings[0].operations, 0);
5173 assert!(timings[1].operations > 0);
5174 assert!(timings[2].operations > timings[1].operations);
5175 assert!(timings.iter().all(|timing| timing.slow && timing.succeeded));
5176 }
5177
5178 #[test]
5179 fn independent_candidates_never_share_callback_authority() {
5180 let mut runtime = RuntimeEngine::new();
5181 let first = runtime
5182 .compile("fn view() { text(\"first\") } fn action() { 1 }")
5183 .unwrap();
5184 let callback = runtime.callback(&first, "action").unwrap();
5185 let second = runtime
5186 .compile("fn view() { text(\"second\") } fn action() { 2 }")
5187 .unwrap();
5188 assert_ne!(first.generation(), second.generation());
5189 runtime.render(&second).unwrap();
5190 assert!(matches!(
5191 runtime.invoke_callback(&second, &callback, ()),
5192 Err(RuntimeError::StaleCallback { .. })
5193 ));
5194 }
5195
5196 #[test]
5197 fn callbacks_cannot_cross_runtime_engines() {
5198 let mut first_engine = RuntimeEngine::new();
5199 let first = first_engine
5200 .compile("fn view() { text(\"first\") } fn action() { 1 }")
5201 .unwrap();
5202 first_engine.render(&first).unwrap();
5203 let callback = first_engine.callback(&first, "action").unwrap();
5204
5205 let mut second_engine = RuntimeEngine::new();
5206 let second = second_engine
5207 .compile("fn view() { text(\"second\") } fn action() { 2 }")
5208 .unwrap();
5209 second_engine.render(&second).unwrap();
5210 assert!(matches!(
5211 second_engine.invoke_callback(&second, &callback, ()),
5212 Err(RuntimeError::StaleCallback { .. })
5213 ));
5214 }
5215
5216 #[test]
5217 fn one_program_preparation_shares_identity_across_module_and_entry_compilation() {
5218 let mut runtime = RuntimeEngine::new();
5219 let (module, entry) = runtime
5220 .with_program_preparation(|runtime| {
5221 Ok::<_, RuntimeError>((
5222 runtime.compile_named("module", "fn helper() { 1 }")?,
5223 runtime.compile_named("entry", "fn view() { text(\"entry\") }")?,
5224 ))
5225 })
5226 .unwrap();
5227 assert_eq!(module.generation(), entry.generation());
5228 let next = runtime.compile("fn view() { text(\"next\") }").unwrap();
5229 assert_ne!(entry.generation(), next.generation());
5230 }
5231
5232 #[test]
5233 fn unsafe_const_container_assignment_is_rejected_before_evaluation() {
5234 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
5235 RuntimeEngine::new()
5236 .compile("fn view() { const m = #{ x: 1 }; m.x = 2; text(\"done\") }")
5237 }));
5238 assert!(matches!(
5239 result,
5240 Ok(Err(RuntimeError::InvalidAssignmentTarget(_)))
5241 ));
5242 }
5243
5244 #[test]
5245 fn default_runtime_rejects_filesystem_imports() {
5246 let directory = tempfile::tempdir().unwrap();
5247 let module = directory.path().join("external.rhai");
5248 std::fs::write(&module, "fn value() { \"external\" }").unwrap();
5249 let mut runtime = RuntimeEngine::new();
5250 let source = format!(
5251 "import {:?} as external; fn view() {{ text(external::value()) }}",
5252 module.with_extension("").to_string_lossy()
5253 );
5254 let compiled = runtime.compile(&source).unwrap();
5255 assert!(runtime.render(&compiled).is_err());
5256 }
5257
5258 #[test]
5259 fn nested_component_work_consumes_one_shared_operation_budget() {
5260 let module = r#"
5261 define_component(#{
5262 metadata: #{ id: "components/heavy", "export": "Heavy", version: "0.1.1",
5263 runtime_api: #{ min_inclusive: 3, max_exclusive: 4 }, dependencies: [], capabilities: #{} },
5264 schema: #{ props: #{}, state: #{ fields: #{} }, events: #{}, slots: #{}, parts: ["root"] },
5265 render: Fn("render_Heavy")
5266 });
5267 fn Heavy(props) { render_component("components/heavy", props) }
5268 fn render_Heavy(ctx, props) {
5269 let n = 0;
5270 for i in 0..200000 { n += 1; }
5271 text(`${n}`)
5272 }
5273 "#;
5274 let mut resolver = crate::RestrictedModuleResolver::new();
5275 resolver.insert("components/heavy", module).unwrap();
5276 let mut runtime = RuntimeEngine::new();
5277 runtime.set_module_resolver(resolver);
5278 let compiled = runtime
5279 .compile_self_contained_named(
5280 "budget",
5281 r#"import "components/heavy" as h;
5282 fn view() { column([h::Heavy(#{}), h::Heavy(#{}), h::Heavy(#{}), h::Heavy(#{} )]) }"#,
5283 )
5284 .unwrap();
5285 let result = runtime.render(&compiled);
5286 assert!(result.is_err(), "nested work must exceed the shared budget");
5287 assert!(
5288 runtime
5289 .take_timings()
5290 .iter()
5291 .any(|timing| timing.operations > DEFAULT_SCRIPT_OPERATION_LIMIT)
5292 );
5293 }
5294
5295 #[test]
5296 fn operation_limit_is_host_configurable() {
5297 let heavy = r"fn view() { let n = 0; for i in 0..400000 { n += 1; } text(`${n}`) }";
5298 let mut runtime = RuntimeEngine::new();
5299 assert_eq!(runtime.operation_limit(), DEFAULT_SCRIPT_OPERATION_LIMIT);
5300 let compiled = runtime
5301 .compile_self_contained_named("limit", heavy)
5302 .unwrap();
5303 assert!(
5304 runtime.render(&compiled).is_err(),
5305 "this finite workload actually exceeds the default budget"
5306 );
5307 assert!(runtime.last_failed_timing().unwrap().operations > DEFAULT_SCRIPT_OPERATION_LIMIT);
5308
5309 runtime.set_operation_limit(10_000);
5310 let compiled = runtime
5311 .compile_self_contained_named("limit-low", heavy)
5312 .unwrap();
5313 assert!(
5314 runtime.render(&compiled).is_err(),
5315 "a lowered budget rejects the same script"
5316 );
5317
5318 runtime.set_operation_limit(50_000_000);
5319 let compiled = runtime
5320 .compile_self_contained_named("limit-high", heavy)
5321 .unwrap();
5322 assert!(
5323 runtime.render(&compiled).is_ok(),
5324 "a raised budget admits heavier one-shot work"
5325 );
5326 }
5327
5328 #[test]
5329 fn host_policies_are_independent_and_reload_candidates_do_not_share_mutable_quota() {
5330 let mut engine = RuntimeEngine::new();
5331 assert_eq!(engine.expression_depth_limits(), (64, 32));
5332 engine.set_operation_limit(2_000_000);
5333 engine.set_expression_depth_limits(80, 64);
5334 assert_eq!(engine.engine().max_call_levels(), 64);
5335 assert_eq!(engine.engine().max_array_size(), RHAI_MAX_ARRAY_SIZE);
5336 #[cfg(feature = "dev-reload")]
5337 {
5338 let mut candidate = engine.candidate_engine();
5339 assert_eq!(candidate.operation_limit(), 2_000_000);
5340 assert_eq!(candidate.expression_depth_limits(), (80, 64));
5341 candidate.set_operation_limit(10);
5342 candidate.set_expression_depth_limits(20, 10);
5343 assert_eq!(engine.operation_limit(), 2_000_000);
5344 assert_eq!(engine.expression_depth_limits(), (80, 64));
5345 }
5346 engine.set_operation_limit(0);
5347 engine.set_expression_depth_limits(0, 0);
5348 assert_eq!(engine.operation_limit(), 1);
5349 assert_eq!(engine.expression_depth_limits(), (1, 1));
5350 }
5351
5352 #[test]
5353 fn retained_component_budget_ignores_parent_history_without_resetting_session_total() {
5354 let module = r#"
5355 define_component(#{
5356 metadata: #{ id: "components/probe", "export": "Probe", version: "0.1.1",
5357 runtime_api: #{ min_inclusive: 3, max_exclusive: 4 }, dependencies: [], capabilities: #{} },
5358 schema: #{ props: #{ key: #{ schema: #{ type: "string" }, required: true, sensitive: false } },
5359 state: #{ fields: #{ heavy: #{ schema: #{ type: "bool" }, "default": #{ type: "bool", value: false } } } },
5360 events: #{}, slots: #{}, parts: ["root"] },
5361 render: Fn("render_Probe")
5362 });
5363 fn Probe(props) { render_component("components/probe", props) }
5364 fn render_Probe(ctx, props) {
5365 let n = 0;
5366 if ctx.get_state("heavy") { for i in 0..100000 { n += 1; } }
5367 text(`${n}`)
5368 }
5369 "#;
5370 let mut measured = Vec::new();
5371 for parent_work in [0, 250_000] {
5372 let mut resolver = crate::RestrictedModuleResolver::new();
5373 resolver.insert("components/probe", module).unwrap();
5374 let mut engine = RuntimeEngine::new();
5375 engine.set_module_resolver(resolver);
5376 let source = format!(
5377 r#"import "components/probe" as probe;
5378 fn view(ctx) {{
5379 let n = 0;
5380 for i in 0..{parent_work} {{ n += 1; }}
5381 probe::Probe(#{{ key: "probe" }})
5382 }}"#
5383 );
5384 let compiled = engine
5385 .compile_self_contained_named("incremental-budget", &source)
5386 .unwrap();
5387 let runtime = Rc::new(RefCell::new(crate::UiRuntimeState::new()));
5388 let mut lifecycle = crate::ScriptLifecycle::new(
5389 compiled,
5390 Rc::clone(&runtime),
5391 ComponentInstancePath::root("App", "budget"),
5392 None,
5393 BTreeMap::new(),
5394 &ComponentStateSchema::default(),
5395 )
5396 .unwrap();
5397 lifecycle.start(&mut engine).unwrap();
5398 let owner = engine
5399 .component_invocations()
5400 .next()
5401 .unwrap()
5402 .path()
5403 .clone();
5404 let _ = engine.take_timings();
5405 runtime
5406 .borrow_mut()
5407 .set_component_state_from_host(&owner, "heavy", UiValue::Bool(true))
5408 .unwrap();
5409 assert!(lifecycle.render_dirty(&mut engine).unwrap());
5410 let operations = engine
5411 .take_timings()
5412 .into_iter()
5413 .filter(|timing| timing.operation == ExecutionOperation::Render)
5414 .map(|timing| timing.operations)
5415 .sum::<u64>();
5416 assert!(
5417 operations < DEFAULT_SCRIPT_OPERATION_LIMIT / 2,
5418 "{operations}"
5419 );
5420 measured.push(operations);
5421 }
5422 assert_eq!(measured[0], measured[1]);
5423 }
5424
5425 #[test]
5426 fn multiple_dirty_components_share_one_incremental_budget() {
5427 let module = r#"
5428 define_component(#{
5429 metadata: #{ id: "components/probe", "export": "Probe", version: "0.1.1",
5430 runtime_api: #{ min_inclusive: 3, max_exclusive: 4 }, dependencies: [], capabilities: #{} },
5431 schema: #{ props: #{ key: #{ schema: #{ type: "string" }, required: true, sensitive: false } },
5432 state: #{ fields: #{ heavy: #{ schema: #{ type: "bool" }, "default": #{ type: "bool", value: false } } } },
5433 events: #{}, slots: #{}, parts: ["root"] }, render: Fn("render_Probe") }
5434 );
5435 fn Probe(props) { render_component("components/probe", props) }
5436 fn render_Probe(ctx, props) {
5437 let n = 0;
5438 if ctx.get_state("heavy") { for i in 0..200000 { n += 1; } }
5439 text(`${n}`)
5440 }
5441 "#;
5442 let mut resolver = crate::RestrictedModuleResolver::new();
5443 resolver.insert("components/probe", module).unwrap();
5444 let mut engine = RuntimeEngine::new();
5445 engine.set_module_resolver(resolver);
5446 let compiled = engine
5447 .compile_self_contained_named(
5448 "incremental-aggregate-budget",
5449 r#"import "components/probe" as probe;
5450 fn view(ctx) {
5451 column([
5452 probe::Probe(#{ key: "first" }),
5453 probe::Probe(#{ key: "second" }),
5454 ])
5455 }"#,
5456 )
5457 .unwrap();
5458 let runtime = Rc::new(RefCell::new(crate::UiRuntimeState::new()));
5459 let mut lifecycle = crate::ScriptLifecycle::new(
5460 compiled,
5461 Rc::clone(&runtime),
5462 ComponentInstancePath::root("App", "budget"),
5463 None,
5464 BTreeMap::new(),
5465 &ComponentStateSchema::default(),
5466 )
5467 .unwrap();
5468 lifecycle.start(&mut engine).unwrap();
5469 let owners = engine
5470 .component_invocations()
5471 .map(|recipe| recipe.path().clone())
5472 .collect::<Vec<_>>();
5473 assert_eq!(owners.len(), 2);
5474 for owner in owners {
5475 runtime
5476 .borrow_mut()
5477 .set_component_state_from_host(&owner, "heavy", UiValue::Bool(true))
5478 .unwrap();
5479 }
5480 assert!(lifecycle.render_dirty(&mut engine).is_err());
5481 }
5482
5483 #[test]
5484 fn retained_virtual_renderer_budget_ignores_parent_history() {
5485 let mut measured = Vec::new();
5486 for parent_work in [0, 250_000] {
5487 let mut engine = RuntimeEngine::new();
5488 let source = format!(
5489 r#"
5490 fn render_item(ctx, payload) {{
5491 let n = 0;
5492 if payload.item.heavy {{ for i in 0..100000 {{ n += 1; }} }}
5493 text(`${{n}}`).with_key(payload.key)
5494 }}
5495 fn view(ctx) {{
5496 let n = 0;
5497 for i in 0..{parent_work} {{ n += 1; }}
5498 let data = [];
5499 for index in 0..32 {{
5500 data.push(#{{ key: `row-${{index}}`, heavy: index == 31 }});
5501 }}
5502 virtual_collection(#{{
5503 key: "rows", label: "Rows", data: data,
5504 estimated_height: 24, height: 24, overdraw_pixels: 0,
5505 alignment: "top", follow_tail: false,
5506 }}, Fn("render_item"))
5507 }}
5508 "#
5509 );
5510 let compiled = engine.compile_named("virtual-budget", &source).unwrap();
5511 let runtime = Rc::new(RefCell::new(crate::UiRuntimeState::new()));
5512 let root_path = ComponentInstancePath::root("App", "budget");
5513 let mut lifecycle = crate::ScriptLifecycle::new(
5514 compiled,
5515 Rc::clone(&runtime),
5516 root_path.clone(),
5517 None,
5518 BTreeMap::new(),
5519 &ComponentStateSchema::default(),
5520 )
5521 .unwrap();
5522 lifecycle.start(&mut engine).unwrap();
5523 let id = engine
5524 .virtual_collection_ids_in_scope(&root_path)
5525 .into_iter()
5526 .next()
5527 .unwrap();
5528 let _ = engine.take_timings();
5529 runtime.borrow().virtual_requests.request(id, [31]);
5530 assert!(lifecycle.realize_virtual_requests(&mut engine).unwrap());
5531 let operations = engine
5532 .take_timings()
5533 .into_iter()
5534 .filter(|timing| {
5535 matches!(timing.operation, ExecutionOperation::VirtualCollection(_))
5536 })
5537 .map(|timing| timing.operations)
5538 .sum::<u64>();
5539 assert!(
5540 operations < DEFAULT_SCRIPT_OPERATION_LIMIT / 2,
5541 "{operations}"
5542 );
5543 measured.push(operations);
5544 }
5545 assert_eq!(measured[0], measured[1]);
5546 }
5547
5548 #[test]
5549 fn retained_virtual_renderer_replays_curried_node_presentation() {
5550 let mut engine = RuntimeEngine::new();
5551 let compiled = engine
5552 .compile_named(
5553 "virtual-rich-presentation",
5554 r#"
5555 fn render_item(content, ctx, payload) {
5556 content[payload.key]
5557 }
5558 fn view(ctx) {
5559 let data = [];
5560 let content = #{};
5561 for index in 0..32 {
5562 let key = `row-${index}`;
5563 data.push(#{ key: key });
5564 content[key] = text(`Rich ${index}`);
5565 }
5566 virtual_collection(#{
5567 key: "rows", label: "Rows", data: data,
5568 estimated_height: 24, height: 24, overdraw_pixels: 0,
5569 alignment: "top", follow_tail: false,
5570 }, Fn("render_item").curry(content))
5571 }
5572 "#,
5573 )
5574 .unwrap();
5575 let runtime = Rc::new(RefCell::new(crate::UiRuntimeState::new()));
5576 let root_path = ComponentInstancePath::root("App", "rich");
5577 let mut lifecycle = crate::ScriptLifecycle::new(
5578 compiled,
5579 Rc::clone(&runtime),
5580 root_path.clone(),
5581 None,
5582 BTreeMap::new(),
5583 &ComponentStateSchema::default(),
5584 )
5585 .unwrap();
5586 lifecycle.start(&mut engine).unwrap();
5587 let id = engine
5588 .virtual_collection_ids_in_scope(&root_path)
5589 .into_iter()
5590 .next()
5591 .unwrap();
5592 runtime.borrow().virtual_requests.request(id, [31]);
5593 assert!(lifecycle.realize_virtual_requests(&mut engine).unwrap());
5594 let crate::UiNodeKind::VirtualCollection { spec } = lifecycle.root().unwrap().kind() else {
5595 unreachable!()
5596 };
5597 assert!(matches!(
5598 spec.realized[&31].kind(),
5599 crate::UiNodeKind::Text { text } if text == "Rich 31"
5600 ));
5601 }
5602
5603 #[test]
5604 #[allow(clippy::too_many_lines)]
5605 fn virtual_row_formal_component_preserves_the_real_callback_owner() {
5606 let mut engine = RuntimeEngine::new();
5607 let compiled = engine
5608 .compile_named(
5609 "virtual-callback-owner",
5610 r#"
5611 define_component(#{
5612 metadata: #{ id: "test/action", "export": "Action", version: "0.1.8",
5613 runtime_api: #{ min_inclusive: 3, max_exclusive: 4 },
5614 dependencies: [], capabilities: #{} },
5615 schema: #{ props: #{
5616 key: #{ schema: #{ type: "string" }, required: true, sensitive: false },
5617 on_action: #{ schema: #{ type: "callback" }, required: true, sensitive: false },
5618 }, state: #{ fields: #{} }, events: #{}, slots: #{}, parts: [] },
5619 render: Fn("render_Action")
5620 });
5621 fn Action(props) { render_component("test/action", props) }
5622 fn render_Action(ctx, props) { text("Action").on_click(props.on_action) }
5623 fn increment(ctx, payload) { ctx.set_state("count", ctx.get_state("count") + 1); }
5624 fn render_item(ctx, payload) {
5625 Action(#{ key: payload.key, on_action: Fn("increment") })
5626 }
5627 fn view(ctx) {
5628 let data=[];for index in 0..32 {data.push(#{key:`row-${index}`});}
5629 virtual_collection(#{
5630 key: "rows", label: "Rows", data: data,
5631 estimated_height: 24, height: 24, overdraw_pixels: 0,
5632 alignment: "top", follow_tail: false,
5633 }, Fn("render_item"))
5634 }
5635 "#,
5636 )
5637 .unwrap();
5638 let runtime = Rc::new(RefCell::new(crate::UiRuntimeState::new()));
5639 let root_path = ComponentInstancePath::root("App", "virtual-callback");
5640 let schema = ComponentStateSchema::new(BTreeMap::from([(
5641 "count".to_owned(),
5642 crate::StateField::new(
5643 crate::ValueSchema::Integer {
5644 min: None,
5645 max: None,
5646 },
5647 UiValue::Integer(0),
5648 ),
5649 )]))
5650 .unwrap();
5651 let mut lifecycle = crate::ScriptLifecycle::new(
5652 compiled,
5653 Rc::clone(&runtime),
5654 root_path.clone(),
5655 None,
5656 BTreeMap::new(),
5657 &schema,
5658 )
5659 .unwrap();
5660 lifecycle.start(&mut engine).unwrap();
5661 let crate::UiNodeKind::VirtualCollection { spec } = lifecycle.root().unwrap().kind() else {
5662 unreachable!()
5663 };
5664 let crate::UiEventHandler::Script(callback) =
5665 spec.realized[&0].handlers()["click"][0].handler()
5666 else {
5667 panic!("formal row component must retain its callback");
5668 };
5669 let _ = lifecycle
5670 .invoke_callback_transactional(&engine, callback, UiValue::Null)
5671 .unwrap();
5672 assert_eq!(
5673 runtime.borrow().component_state.get(&root_path, "count"),
5674 Some(&UiValue::Integer(1))
5675 );
5676 let id = engine
5677 .virtual_collection_ids_in_scope(&root_path)
5678 .into_iter()
5679 .next()
5680 .unwrap();
5681 assert!(
5682 !lifecycle
5683 .root()
5684 .unwrap()
5685 .virtual_collection_items(&id)
5686 .unwrap()
5687 .contains_key(&31)
5688 );
5689 runtime.borrow().virtual_requests.request(id.clone(), [31]);
5690 lifecycle.realize_virtual_requests(&mut engine).unwrap();
5691 let callback = lifecycle
5692 .root()
5693 .unwrap()
5694 .virtual_collection_items(&id)
5695 .unwrap()[&31]
5696 .handler("click")
5697 .unwrap()
5698 .as_script()
5699 .unwrap();
5700 let _ = lifecycle
5701 .invoke_callback_transactional(&engine, callback, UiValue::Null)
5702 .unwrap();
5703 assert_eq!(
5704 runtime.borrow().component_state.get(&root_path, "count"),
5705 Some(&UiValue::Integer(2))
5706 );
5707 }
5708
5709 #[test]
5710 fn invalid_state_default_reports_the_field_and_tagged_encoding() {
5711 let mut engine = RuntimeEngine::new();
5712 let compiled = engine.compile(r#"
5713 fn state_schema() {#{fields:#{current_theme:#{schema:#{type:"map",values:#{type:"ui_value"}},
5714 "default":#{type:"map",value:#{family:"Default"}}}}}}
5715 fn view(ctx) {text("view")}
5716 "#).unwrap();
5717 let error = engine.root_state_schema(&compiled).unwrap_err().to_string();
5718 assert!(
5719 error.contains("state_schema.fields.current_theme.default"),
5720 "{error}"
5721 );
5722 assert!(error.contains("recursively tagged UiValue"), "{error}");
5723 }
5724
5725 #[test]
5726 fn delayed_component_callback_starts_with_a_fresh_operation_budget() {
5727 let module = r#"
5728 define_component(#{
5729 metadata: #{ id: "components/counter", "export": "Counter", version: "0.1.1",
5730 runtime_api: #{ min_inclusive: 3, max_exclusive: 4 }, dependencies: [], capabilities: #{} },
5731 schema: #{ props: #{}, state: #{ fields: #{} }, events: #{}, slots: #{}, parts: ["root"] },
5732 render: Fn("render_Counter")
5733 });
5734 fn Counter(props) { render_component("components/counter", props) }
5735 fn render_Counter(ctx, props) { text("counter").on_click(Fn("clicked")) }
5736 fn clicked(ctx, payload) {
5737 let n = 0;
5738 for i in 0..100000 { n += 1; }
5739 n
5740 }
5741 "#;
5742 let mut resolver = crate::RestrictedModuleResolver::new();
5743 resolver.insert("components/counter", module).unwrap();
5744 let mut engine = RuntimeEngine::new();
5745 engine.set_module_resolver(resolver);
5746 let compiled = engine
5747 .compile_self_contained_named(
5748 "callback-budget",
5749 r#"import "components/counter" as c;
5750 fn view(ctx) {
5751 let n = 0;
5752 for i in 0..250000 { n += 1; }
5753 c::Counter(#{} )
5754 }"#,
5755 )
5756 .unwrap();
5757 let runtime = Rc::new(RefCell::new(UiRuntimeState::new()));
5758 let mut lifecycle = crate::ScriptLifecycle::new(
5759 compiled,
5760 runtime,
5761 ComponentInstancePath::root("App", "budget"),
5762 None,
5763 BTreeMap::new(),
5764 &ComponentStateSchema::default(),
5765 )
5766 .unwrap();
5767 let root = lifecycle.start(&mut engine).unwrap();
5768 let crate::UiEventHandler::Script(callback) = root.handlers()["click"][0].handler() else {
5769 panic!("component must expose a script click callback");
5770 };
5771 let callback = callback.clone();
5772 assert!(
5773 lifecycle
5774 .invoke_callback_transactional(&engine, &callback, UiValue::Null)
5775 .is_ok()
5776 );
5777 }
5778
5779 #[test]
5780 fn old_component_handles_cannot_target_a_same_key_remount() {
5781 let module = r#"
5782 define_component(#{
5783 metadata: #{ id: "components/incarnation", "export": "Counter", version: "0.1.1",
5784 runtime_api: #{ min_inclusive: 3, max_exclusive: 4 }, dependencies: [], capabilities: #{} },
5785 schema: #{ props: #{ key: #{ schema: #{ type: "string" }, required: true, sensitive: false } },
5786 state: #{ fields: #{ count: #{ schema: #{ type: "integer" }, "default": #{ type: "integer", value: 0 } } } },
5787 events: #{}, slots: #{}, parts: ["root"] }, render: Fn("render_Counter")
5788 });
5789 fn Counter(props) { render_component("components/incarnation", props) }
5790 fn increment(ctx, payload) { ctx.set_state("count", ctx.get_state("count") + 1); }
5791 fn render_Counter(ctx, props) {
5792 let progress = signal("progress", 0.0);
5793 text(`${ctx.get_state("count")}`).bind_signal("opacity", progress).on_click(Fn("increment"))
5794 }
5795 "#;
5796 let mut resolver = crate::RestrictedModuleResolver::new();
5797 resolver.insert("components/incarnation", module).unwrap();
5798 let mut engine = RuntimeEngine::new();
5799 engine.set_module_resolver(resolver);
5800 let compiled = engine
5801 .compile_self_contained_named(
5802 "incarnation",
5803 r#"import "components/incarnation" as c;
5804 fn view(ctx) {
5805 if ctx.get_state("visible") { c::Counter(#{ key: "same" }) }
5806 else { text("hidden") }
5807 }"#,
5808 )
5809 .unwrap();
5810 let runtime = Rc::new(RefCell::new(UiRuntimeState::new()));
5811 let root_path = ComponentInstancePath::root("App", "incarnation");
5812 let schema = ComponentStateSchema::new(BTreeMap::from([(
5813 "visible".to_owned(),
5814 crate::StateField::new(crate::ValueSchema::Bool, UiValue::Bool(true)),
5815 )]))
5816 .unwrap();
5817 let mut lifecycle = crate::ScriptLifecycle::new(
5818 compiled,
5819 Rc::clone(&runtime),
5820 root_path.clone(),
5821 None,
5822 BTreeMap::new(),
5823 &schema,
5824 )
5825 .unwrap();
5826 lifecycle.start(&mut engine).unwrap();
5827 let component = engine
5828 .component_invocations()
5829 .next()
5830 .unwrap()
5831 .path()
5832 .clone();
5833 let crate::UiEventHandler::Script(old_callback) =
5834 lifecycle.root().unwrap().handlers()["click"][0].handler()
5835 else {
5836 panic!("component must expose its callback");
5837 };
5838 let old_callback = old_callback.clone();
5839 let old_signal = runtime
5840 .borrow()
5841 .signals
5842 .resolve(&component, "progress")
5843 .unwrap();
5844 runtime
5845 .borrow_mut()
5846 .set_component_state_from_host(&root_path, "visible", UiValue::Bool(false))
5847 .unwrap();
5848 lifecycle.render_dirty(&mut engine).unwrap();
5849 runtime
5850 .borrow_mut()
5851 .set_component_state_from_host(&root_path, "visible", UiValue::Bool(true))
5852 .unwrap();
5853 lifecycle.render_dirty(&mut engine).unwrap();
5854 assert!(matches!(
5855 lifecycle.invoke_callback_transactional(&engine, &old_callback, UiValue::Null),
5856 Err(crate::LifecycleError::Runtime(
5857 RuntimeError::StaleComponentCallback { .. }
5858 ))
5859 ));
5860 assert!(matches!(
5861 runtime
5862 .borrow_mut()
5863 .signals
5864 .write(&old_signal, crate::SignalValue::Float(0.5)),
5865 Err(crate::SignalError::Stale(_))
5866 ));
5867 }
5868}