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 = invocation.call::<UiNode>(
1592 self.engine(),
1593 &recipe.renderer,
1594 (
1595 context.for_virtual_item(id, &key),
1596 Dynamic::from_map(payload),
1597 ),
1598 );
1599 self.record_timing(
1600 ExecutionOperation::VirtualCollection(id.key.clone()),
1601 id.key.as_str(),
1602 started,
1603 item.is_ok(),
1604 );
1605 let mut node = item.map_err(RuntimeError::Evaluate)?.with_key(key);
1606 node.bind_generation(recipe.generation);
1607 node.bind_component_scope(
1608 recipe.context.callback_component_path(),
1609 recipe.context.callback_component_incarnation(),
1610 recipe.context.callback_event_schemas(),
1611 Some(&recipe.renderer_context),
1612 );
1613 realized.insert(index, node);
1614 }
1615 Ok(realized)
1616 })();
1617 if result.is_err() {
1618 record_component_render_failure(&self.component_render, context.component_path());
1619 }
1620 self.finish_component_render(result.is_ok())?;
1621 result
1622 }
1623
1624 pub(crate) fn update_virtual_collection_snapshot(
1625 &mut self,
1626 id: &crate::VirtualCollectionId,
1627 items: &BTreeMap<usize, UiNode>,
1628 ) {
1629 let mut path = Some(id.component.clone());
1633 while let Some(current_path) = path {
1634 if let Some(owner) = self.component_invocations.get_mut(¤t_path)
1635 && let Some(current) = owner.snapshot.current()
1636 && current.virtual_collection_spec(id).is_some()
1637 {
1638 let mut rendered = current.as_ref().clone();
1639 rendered.replace_virtual_collection_items(id, items.clone());
1640 owner.snapshot.replace_if_active(Rc::new(rendered));
1641 }
1642 path = current_path.parent();
1643 }
1644 }
1645
1646 pub fn call_optional_lifecycle(
1652 &self,
1653 compiled: &CompiledUi,
1654 function: &str,
1655 context: UiContext,
1656 ) -> Result<bool, RuntimeError> {
1657 if !compiled.has_function(function, 1) {
1658 return Ok(false);
1659 }
1660 self.evaluation_generation.set(compiled.generation);
1661 self.begin_execution_session();
1662 let started = self.begin_timing();
1663 let result = AstInterpreter::call_fn::<Dynamic, _>(
1664 &self.engine,
1665 &compiled.ast,
1666 &mut Scope::new(),
1667 function,
1668 (context,),
1669 );
1670 self.record_timing(
1671 ExecutionOperation::Lifecycle(function.to_owned()),
1672 compiled.ast.source().unwrap_or("<script>"),
1673 started,
1674 result.is_ok(),
1675 );
1676 let _ = result.map_err(RuntimeError::Evaluate)?;
1677 Ok(true)
1678 }
1679
1680 pub(crate) fn call_optional_lifecycle_with_value(
1681 &self,
1682 compiled: &CompiledUi,
1683 function: &str,
1684 context: UiContext,
1685 value: UiValue,
1686 ) -> Result<bool, RuntimeError> {
1687 if !compiled.has_function(function, 2) {
1688 return Ok(false);
1689 }
1690 self.evaluation_generation.set(compiled.generation);
1691 self.begin_execution_session();
1692 let started = self.begin_timing();
1693 let result = AstInterpreter::call_fn::<Dynamic, _>(
1694 &self.engine,
1695 &compiled.ast,
1696 &mut Scope::new(),
1697 function,
1698 (context, value.into_dynamic()),
1699 );
1700 self.record_timing(
1701 ExecutionOperation::Lifecycle(function.to_owned()),
1702 compiled.ast.source().unwrap_or("<script>"),
1703 started,
1704 result.is_ok(),
1705 );
1706 let _ = result.map_err(RuntimeError::Evaluate)?;
1707 Ok(true)
1708 }
1709
1710 pub fn root_state_schema(
1717 &self,
1718 compiled: &CompiledUi,
1719 ) -> Result<ComponentStateSchema, RuntimeError> {
1720 if !compiled.has_function("state_schema", 0) {
1721 return Ok(ComponentStateSchema::default());
1722 }
1723 self.begin_execution_session();
1724 let started = self.begin_timing();
1725 let result = (|| {
1726 let raw: Dynamic = self
1727 .engine
1728 .call_fn(&mut Scope::new(), &compiled.ast, "state_schema", ())
1729 .map_err(RuntimeError::Evaluate)?;
1730 validate_state_default_encoding(&raw, "state_schema")
1731 .map_err(RuntimeError::Evaluate)?;
1732 rhai::serde::from_dynamic(&raw).map_err(RuntimeError::Evaluate)
1733 })();
1734 self.record_timing(
1735 ExecutionOperation::Lifecycle("state_schema".to_owned()),
1736 compiled.ast.source().unwrap_or("<script>"),
1737 started,
1738 result.is_ok(),
1739 );
1740 result
1741 }
1742
1743 pub fn callback(
1750 &self,
1751 compiled: &CompiledUi,
1752 function_name: &str,
1753 ) -> Result<ScriptCallback, RuntimeError> {
1754 let function =
1755 FnPtr::new(function_name).map_err(|source| RuntimeError::CallbackDefinition {
1756 name: function_name.to_owned(),
1757 source,
1758 })?;
1759 ScriptCallback::try_from_fn_ptr(function, compiled.generation).map_err(|source| {
1760 RuntimeError::RetainedCallback {
1761 name: function_name.to_owned(),
1762 source,
1763 }
1764 })
1765 }
1766
1767 pub fn invoke_callback(
1774 &self,
1775 compiled: &CompiledUi,
1776 callback: &ScriptCallback,
1777 args: impl FuncArgs,
1778 ) -> Result<Dynamic, RuntimeError> {
1779 if !self.is_current(callback.generation) || callback.generation != compiled.generation {
1780 return Err(RuntimeError::StaleCallback {
1781 name: callback.name().to_owned(),
1782 callback_generation: callback.generation,
1783 current_generation: self.generation,
1784 });
1785 }
1786
1787 self.invoke_callback_for_generation(compiled, callback, args)
1788 }
1789
1790 pub(crate) fn invoke_callback_for_generation(
1791 &self,
1792 compiled: &CompiledUi,
1793 callback: &ScriptCallback,
1794 args: impl FuncArgs,
1795 ) -> Result<Dynamic, RuntimeError> {
1796 if callback.generation != compiled.generation {
1797 return Err(RuntimeError::StaleCallback {
1798 name: callback.name().to_owned(),
1799 callback_generation: callback.generation,
1800 current_generation: compiled.generation,
1801 });
1802 }
1803 self.evaluation_generation.set(compiled.generation);
1804 let operation_base = callback.native_context.as_ref().map_or(
1805 0,
1806 crate::invocation::ScriptInvocationContext::operation_base,
1807 );
1808 self.begin_execution_session_from(operation_base);
1809 let started = self.begin_timing();
1810 #[allow(deprecated)]
1811 let result = if let Some(context) = callback.native_context.as_ref() {
1812 context.call(self.engine(), &callback.function, args)
1813 } else {
1814 callback.function.call(self.engine(), &compiled.ast, args)
1815 };
1816 self.record_timing(
1817 ExecutionOperation::Callback(callback.name().to_owned()),
1818 compiled.ast.source().unwrap_or("<script>"),
1819 started,
1820 result.is_ok(),
1821 );
1822 result.map_err(RuntimeError::Evaluate)
1823 }
1824
1825 #[must_use]
1826 pub fn is_current(&self, generation: ScriptGeneration) -> bool {
1827 generation == self.generation
1828 }
1829
1830 #[must_use]
1831 pub fn engine(&self) -> &Engine {
1832 &self.engine
1833 }
1834
1835 #[must_use]
1840 pub fn definition_source(&self) -> String {
1841 self.engine
1842 .definitions()
1843 .include_standard_packages(false)
1844 .single_file()
1845 }
1846
1847 pub fn lint_known_calls(
1859 &mut self,
1860 entry_name: &str,
1861 entry_source: &str,
1862 modules: &BTreeMap<ModuleId, String>,
1863 ) -> Result<Vec<crate::KnownCallDiagnostic>, crate::KnownCallLintError> {
1864 crate::script_lint::lint_known_calls(&mut self.engine, entry_name, entry_source, modules)
1865 }
1866
1867 pub fn component_exports(&self) -> Result<ComponentRegistry, ComponentExportError> {
1874 self.component_exports.snapshot()
1875 }
1876
1877 pub fn component_invocations(
1878 &self,
1879 ) -> impl ExactSizeIterator<Item = &ComponentInvocationRecipe> {
1880 self.component_invocations.values()
1881 }
1882
1883 pub(crate) fn component_renderer_snapshot(
1884 &self,
1885 ) -> Result<BTreeMap<ModuleId, RegisteredComponentRender>, ComponentExportError> {
1886 self.component_renderers
1887 .try_borrow()
1888 .map(|renderers| renderers.clone())
1889 .map_err(|_| ComponentExportError::Borrowed)
1890 }
1891
1892 pub(crate) fn restore_component_renderers(
1893 &self,
1894 renderers: BTreeMap<ModuleId, RegisteredComponentRender>,
1895 ) -> Result<(), ComponentExportError> {
1896 *self
1897 .component_renderers
1898 .try_borrow_mut()
1899 .map_err(|_| ComponentExportError::Borrowed)? = renderers;
1900 Ok(())
1901 }
1902
1903 pub fn clear_component_exports(&self) -> Result<(), ComponentExportError> {
1909 self.component_renderers
1910 .try_borrow_mut()
1911 .map_err(|_| ComponentExportError::Borrowed)?
1912 .clear();
1913 self.component_exports.clear()
1914 }
1915
1916 pub fn restore_component_exports(
1922 &self,
1923 registry: ComponentRegistry,
1924 ) -> Result<(), ComponentExportError> {
1925 self.component_exports.replace(registry)
1926 }
1927
1928 pub fn register_primitive(
1934 &mut self,
1935 descriptor: PrimitiveDescriptor,
1936 handler: impl PrimitiveHandler + 'static,
1937 ) -> Result<(), PrimitiveError> {
1938 self.primitives.register(descriptor.clone(), handler)?;
1939
1940 let namespace = descriptor.id.namespace().to_owned();
1941 let export = descriptor.export.clone();
1942 let id = descriptor.id;
1943 let primitives = self.primitives.clone();
1944 let generation = Rc::clone(&self.evaluation_generation);
1945 let module = self.primitive_modules.entry(namespace.clone()).or_default();
1946 FuncRegistration::new(export).set_into_module(
1947 module,
1948 move |mut props: Map| -> Result<UiNode, Box<EvalAltResult>> {
1949 let key = props
1950 .remove("key")
1951 .map(|value| {
1952 if value.is::<ImmutableString>() {
1953 Ok(value.cast::<ImmutableString>().to_string())
1954 } else {
1955 Err(Box::new(EvalAltResult::ErrorRuntime(
1956 "primitive key must be a string".into(),
1957 Position::NONE,
1958 )))
1959 }
1960 })
1961 .transpose()?;
1962 primitives
1963 .create_node(&id, key, &props, generation.get())
1964 .map_err(|error| {
1965 Box::new(EvalAltResult::ErrorRuntime(
1966 error.to_string().into(),
1967 Position::NONE,
1968 ))
1969 })
1970 },
1971 );
1972 self.engine
1973 .register_static_module(namespace, module.clone().into());
1974 Ok(())
1975 }
1976
1977 #[must_use]
1978 pub fn primitive_registry(&self) -> PrimitiveRegistry {
1979 self.primitives.clone()
1980 }
1981
1982 pub fn register_native_handler(
1988 &self,
1989 descriptor: crate::NativeHandlerDescriptor,
1990 handler: impl FnMut(
1991 crate::NativeEvent,
1992 &mut UiRuntimeState,
1993 &mut gpui::Window,
1994 &mut gpui::App,
1995 ) -> Result<crate::EventResponse, String>
1996 + 'static,
1997 ) -> Result<(), crate::NativeHandlerError> {
1998 self.native_handlers.register(descriptor, handler)
1999 }
2000
2001 #[must_use]
2002 pub fn native_handler_registry(&self) -> crate::NativeHandlerRegistry {
2003 self.native_handlers.clone()
2004 }
2005
2006 #[must_use]
2007 pub fn syntax_registry(&self) -> crate::SyntaxRegistry {
2008 self.syntax_registry.clone()
2009 }
2010
2011 #[must_use]
2012 pub fn document_runtime_config(&self) -> crate::DocumentRuntimeConfig {
2013 self.document_runtime.clone()
2014 }
2015
2016 #[cfg(feature = "charts")]
2022 pub fn register_chart_transform(
2023 &self,
2024 id: impl Into<String>,
2025 transform: impl crate::HostChartTransform + 'static,
2026 ) -> Result<(), crate::ChartTransformError> {
2027 self.chart_transforms.register(id, transform)
2028 }
2029
2030 #[cfg(feature = "charts")]
2036 pub fn register_chart_map(&self, map: crate::ChartGeoMap) -> Result<(), crate::ChartGeoError> {
2037 self.chart_geo.register_map(map)
2038 }
2039
2040 #[cfg(feature = "charts")]
2046 pub fn register_chart_projection(
2047 &self,
2048 id: impl Into<String>,
2049 projection: impl crate::ChartGeoProjection + 'static,
2050 ) -> Result<(), crate::ChartGeoError> {
2051 self.chart_geo.register_projection(id, projection)
2052 }
2053
2054 #[cfg(feature = "charts")]
2060 pub fn register_chart_series(
2061 &self,
2062 id: impl Into<String>,
2063 renderer: impl crate::HostChartSeries + 'static,
2064 ) -> Result<(), crate::ChartSeriesExtensionError> {
2065 self.chart_series.register(id, renderer)
2066 }
2067
2068 #[cfg(feature = "charts")]
2074 pub fn register_chart_formatter(
2075 &self,
2076 id: impl Into<String>,
2077 formatter: impl crate::HostChartFormatter + 'static,
2078 ) -> Result<(), crate::ChartFormatterError> {
2079 self.chart_formatters.register(id, formatter)
2080 }
2081
2082 #[cfg(feature = "charts")]
2083 #[must_use]
2084 pub fn chart_transform_registry(&self) -> crate::ChartTransformRegistry {
2085 self.chart_transforms.clone()
2086 }
2087
2088 #[cfg(feature = "charts")]
2089 #[must_use]
2090 pub fn chart_geo_registry(&self) -> crate::ChartGeoRegistry {
2091 self.chart_geo.clone()
2092 }
2093
2094 #[cfg(feature = "charts")]
2095 #[must_use]
2096 pub fn chart_series_registry(&self) -> crate::ChartSeriesRegistry {
2097 self.chart_series.clone()
2098 }
2099
2100 #[cfg(feature = "charts")]
2101 #[must_use]
2102 pub fn chart_formatter_registry(&self) -> crate::ChartFormatterRegistry {
2103 self.chart_formatters.clone()
2104 }
2105
2106 pub fn set_slow_threshold(&mut self, threshold: Duration) {
2107 self.slow_threshold = threshold;
2108 }
2109
2110 pub fn set_operation_limit(&mut self, limit: u64) {
2122 self.operation_limit.set(limit.max(1));
2123 }
2124
2125 #[must_use]
2127 pub fn operation_limit(&self) -> u64 {
2128 self.operation_limit.get()
2129 }
2130
2131 pub fn set_expression_depth_limits(&mut self, global: usize, functions: usize) {
2135 self.engine
2136 .set_max_expr_depths(global.max(1), functions.max(1));
2137 }
2138
2139 #[must_use]
2140 pub fn expression_depth_limits(&self) -> (usize, usize) {
2141 (
2142 self.engine.max_expr_depth(),
2143 self.engine.max_function_expr_depth(),
2144 )
2145 }
2146
2147 #[must_use]
2148 pub fn take_timings(&self) -> Vec<ExecutionTiming> {
2149 std::mem::take(&mut *self.timings.borrow_mut())
2150 }
2151
2152 #[must_use]
2155 pub fn last_failed_timing(&self) -> Option<ExecutionTiming> {
2156 self.last_failed_timing.borrow().clone()
2157 }
2158
2159 #[must_use]
2160 pub(crate) fn last_failed_component(&self) -> Option<ComponentInstancePath> {
2161 self.last_failed_component.borrow().clone()
2162 }
2163
2164 fn record_timing(
2165 &self,
2166 operation: ExecutionOperation,
2167 source: &str,
2168 started: ExecutionTimingStart,
2169 succeeded: bool,
2170 ) {
2171 let duration = started.instant.elapsed();
2172 let operations = self.operation_total().saturating_sub(started.operations);
2173 let timing = ExecutionTiming {
2174 operation,
2175 source: source.to_owned(),
2176 duration,
2177 operations,
2178 operation_limit: started.operation_limit,
2179 round_operations: self.operation_total(),
2180 operation_semantics: OPERATION_SEMANTICS_VERSION,
2181 slow: duration >= self.slow_threshold,
2182 succeeded,
2183 };
2184 if !succeeded {
2185 *self.last_failed_timing.borrow_mut() = Some(timing.clone());
2186 }
2187 self.timings.borrow_mut().push(timing);
2188 }
2189
2190 pub(crate) fn begin_execution_session(&self) {
2191 self.begin_execution_session_from(0);
2192 }
2193
2194 fn begin_execution_session_from(&self, inherited_base: u64) {
2195 self.last_failed_timing.borrow_mut().take();
2196 self.last_failed_component.borrow_mut().take();
2197 self.operation_tracker.begin(inherited_base);
2198 }
2199
2200 fn align_execution_session_to(&self, inherited_base: u64) {
2201 self.operation_tracker.align(inherited_base);
2202 }
2203
2204 fn operation_total(&self) -> u64 {
2205 self.operation_tracker.total.get()
2206 }
2207
2208 fn begin_timing(&self) -> ExecutionTimingStart {
2209 ExecutionTimingStart {
2210 instant: Instant::now(),
2211 operations: self.operation_total(),
2212 operation_limit: self.operation_limit(),
2213 }
2214 }
2215
2216 fn candidate_generation(&mut self) -> ScriptGeneration {
2217 self.preparation_generation
2218 .unwrap_or_else(Self::allocate_generation)
2219 }
2220
2221 fn allocate_generation() -> ScriptGeneration {
2222 static NEXT_GENERATION: AtomicU64 = AtomicU64::new(1);
2223 ScriptGeneration(NEXT_GENERATION.fetch_add(1, Ordering::Relaxed))
2224 }
2225}
2226
2227pub(crate) fn validate_assignment_targets(ast: &AST) -> Result<(), RuntimeError> {
2228 let mut invalid = None;
2229 ast.walk(&mut |path| {
2230 let Some(ASTNode::Stmt(Stmt::Assignment(assignment))) = path.last() else {
2231 return true;
2232 };
2233 if !matches!(
2234 assignment.1.lhs,
2235 Expr::ThisPtr(_) | Expr::Variable(..) | Expr::Index(..) | Expr::Dot(..)
2236 ) {
2237 invalid = Some(assignment.1.lhs.position());
2238 return false;
2239 }
2240 true
2241 });
2242 invalid.map_or(Ok(()), |position| {
2243 Err(RuntimeError::InvalidAssignmentTarget(position))
2244 })
2245}
2246
2247fn validate_literal_imports(ast: &AST) -> Result<(), RuntimeError> {
2248 let mut dynamic = None;
2249 ast.walk(&mut |path| {
2250 let Some(ASTNode::Stmt(Stmt::Import(import, position))) = path.last() else {
2251 return true;
2252 };
2253 if matches!(import.0, Expr::StringConstant(..)) {
2254 true
2255 } else {
2256 dynamic = Some(*position);
2257 false
2258 }
2259 });
2260 dynamic.map_or(Ok(()), |position| {
2261 Err(RuntimeError::Import(format!(
2262 "Rhai imports must use a literal module string at {position}"
2263 )))
2264 })
2265}
2266
2267fn register_node_apis(engine: &mut Engine) {
2268 FuncRegistration::new("text")
2269 .in_global_namespace()
2270 .register_into_engine(engine, text_node);
2271 FuncRegistration::new("text")
2272 .in_global_namespace()
2273 .register_into_engine(engine, rich_text_node);
2274 FuncRegistration::new("span")
2275 .in_global_namespace()
2276 .register_into_engine(engine, span_value);
2277 FuncRegistration::new("canvas")
2278 .in_global_namespace()
2279 .register_into_engine(engine, canvas_node);
2280 FuncRegistration::new("svg")
2281 .in_global_namespace()
2282 .register_into_engine(engine, svg_node);
2283 FuncRegistration::new("box")
2284 .in_global_namespace()
2285 .register_into_engine(engine, box_node);
2286 FuncRegistration::new("fragment")
2287 .in_global_namespace()
2288 .register_into_engine(engine, fragment_node);
2289 FuncRegistration::new("motion_group")
2290 .in_global_namespace()
2291 .register_into_engine(engine, motion_group_node);
2292 FuncRegistration::new("stack")
2293 .in_global_namespace()
2294 .register_into_engine(engine, stack_node);
2295 FuncRegistration::new("handled")
2296 .in_global_namespace()
2297 .register_into_engine(engine, || crate::EventResponse::new().stop());
2298 FuncRegistration::new("propagate")
2299 .in_global_namespace()
2300 .register_into_engine(engine, crate::EventResponse::new);
2301 FuncRegistration::new("event_response")
2302 .in_global_namespace()
2303 .register_into_engine(engine, crate::EventResponse::new);
2304 FuncRegistration::new("column")
2305 .in_global_namespace()
2306 .register_into_engine(engine, column_node);
2307 FuncRegistration::new("row")
2308 .in_global_namespace()
2309 .register_into_engine(engine, row_node);
2310 FuncRegistration::new("error_boundary")
2311 .in_global_namespace()
2312 .register_into_engine(engine, error_boundary_node);
2313 FuncRegistration::new("error_boundary_lazy")
2314 .in_global_namespace()
2315 .register_into_engine(engine, lazy_error_boundary_node);
2316 FuncRegistration::new("asset")
2317 .in_global_namespace()
2318 .register_into_engine(engine, asset_id_from_script);
2319 FuncRegistration::new("image")
2320 .in_global_namespace()
2321 .register_into_engine(engine, image_node);
2322 FuncRegistration::new("image")
2323 .in_global_namespace()
2324 .register_into_engine(engine, asset_image_node);
2325 FuncRegistration::new("directional_image")
2326 .in_global_namespace()
2327 .register_into_engine(engine, directional_image_node);
2328 FuncRegistration::new("directional_image")
2329 .in_global_namespace()
2330 .register_into_engine(engine, directional_asset_image_node);
2331 FuncRegistration::new("image_source")
2332 .in_global_namespace()
2333 .register_into_engine(engine, generic_image_node);
2334 FuncRegistration::new("directional_image_source")
2335 .in_global_namespace()
2336 .register_into_engine(engine, generic_directional_image_node);
2337 FuncRegistration::new("overlay")
2338 .in_global_namespace()
2339 .register_into_engine(engine, overlay_node);
2340 FuncRegistration::new("layer")
2341 .in_global_namespace()
2342 .register_into_engine(engine, layer_node);
2343}
2344
2345fn register_native_handler_api(engine: &mut Engine, registry: &crate::NativeHandlerRegistry) {
2346 let registry = registry.clone();
2347 FuncRegistration::new("native_handler")
2348 .in_global_namespace()
2349 .register_into_engine(
2350 engine,
2351 move |id: ImmutableString| -> Result<crate::NativeHandlerRef, Box<EvalAltResult>> {
2352 let id = crate::NativeHandlerId::parse(id.to_string())
2353 .map_err(|error| Box::new(component_render_error(error.to_string())))?;
2354 registry
2355 .resolve(&id)
2356 .map_err(|error| Box::new(component_render_error(error.to_string())))
2357 },
2358 );
2359}
2360
2361pub(crate) const RHAI_MAX_ARRAY_SIZE: usize = 10_000;
2362pub(crate) const RHAI_MAX_MAP_SIZE: usize = 100_000;
2363pub(crate) const RHAI_MAX_STRING_SIZE: usize = 1_048_576;
2364pub(crate) const RHAI_MAX_DATA_DEPTH: usize = 64;
2365
2366fn configure_engine_limits(engine: &mut Engine) {
2367 engine.set_max_call_levels(64);
2368 engine.set_max_expr_depths(64, 32);
2372 engine.set_max_operations(0);
2376 engine.set_max_array_size(RHAI_MAX_ARRAY_SIZE);
2377 engine.set_max_map_size(RHAI_MAX_MAP_SIZE);
2378 engine.set_max_string_size(RHAI_MAX_STRING_SIZE);
2379}
2380
2381fn register_define_component_api(
2382 engine: &mut Engine,
2383 exports: &ComponentExportCollector,
2384 renderers: &ComponentRenderRegistry,
2385 generation: &Rc<Cell<ScriptGeneration>>,
2386) {
2387 let exports = exports.clone();
2388 let renderers = Rc::clone(renderers);
2389 let generation = Rc::clone(generation);
2390 FuncRegistration::new("define_component")
2391 .in_global_namespace()
2392 .register_into_engine(
2393 engine,
2394 move |mut raw: Map| -> Result<(), Box<EvalAltResult>> {
2395 let render = raw.remove("render").ok_or_else(|| {
2396 Box::new(component_render_error(
2397 "define_component requires a named `render` function",
2398 ))
2399 })?;
2400 if !render.is::<FnPtr>() {
2401 return Err(Box::new(component_render_error(
2402 "define_component `render` must be a function pointer",
2403 )));
2404 }
2405 let render = render.cast::<FnPtr>();
2406 if render.is_anonymous() || render.is_curried() {
2407 return Err(Box::new(component_render_error(
2408 "formal component render must be an uncurried named function",
2409 )));
2410 }
2411 if let Some(schema) = raw.get("schema")
2412 && schema.is::<Map>()
2413 && let Some(state) = schema.clone_cast::<Map>().get("state")
2414 {
2415 validate_state_default_encoding(state, "component.schema.state")?;
2416 }
2417 let decoded: crate::ComponentDefinition =
2418 rhai::serde::from_dynamic(&Dynamic::from_map(raw))?;
2419 let definition = crate::ComponentDefinition::new(decoded.metadata, decoded.schema)
2420 .map_err(|error| Box::new(component_render_error(error.to_string())))?;
2421 let id = definition.metadata.id.clone();
2422 exports
2423 .register_definition(definition)
2424 .map_err(|error| Box::new(component_render_error(error.to_string())))?;
2425 renderers
2426 .try_borrow_mut()
2427 .map_err(|_| {
2428 Box::new(component_render_error(
2429 "component render registry is already borrowed",
2430 ))
2431 })?
2432 .insert(
2433 id,
2434 RegisteredComponentRender {
2435 generation: generation.get(),
2436 render,
2437 },
2438 );
2439 Ok(())
2440 },
2441 );
2442}
2443
2444fn validate_state_default_encoding(raw: &Dynamic, path: &str) -> Result<(), Box<EvalAltResult>> {
2445 if !raw.is::<Map>() {
2446 return Ok(());
2447 }
2448 let raw = raw.clone_cast::<Map>();
2449 let Some(fields) = raw.get("fields").filter(|fields| fields.is::<Map>()) else {
2450 return Ok(());
2451 };
2452 for (name, field) in fields.clone_cast::<Map>() {
2453 if !field.is::<Map>() {
2454 continue;
2455 }
2456 let field = field.cast::<Map>();
2457 if let Some(value) = field.get("default") {
2458 rhai::serde::from_dynamic::<UiValue>(value).map_err(|source| {
2459 Box::new(component_render_error(format!(
2460 "{path}.fields.{name}.default: expected recursively tagged UiValue; map entries and array items each require type/value descriptors ({source})"
2461 )))
2462 })?;
2463 }
2464 }
2465 Ok(())
2466}
2467
2468fn register_component_runtime_apis(
2469 engine: &mut Engine,
2470 exports: &ComponentExportCollector,
2471 generation: &Rc<Cell<ScriptGeneration>>,
2472) -> (ActiveComponentRenderState, ComponentRenderRegistry) {
2473 let active = Rc::new(RefCell::new(None));
2474 let renderers = Rc::new(RefCell::new(BTreeMap::new()));
2475 register_define_component_api(engine, exports, &renderers, generation);
2476 register_render_component_api(engine, exports, &renderers, &active);
2477 register_effect_api(engine, &active);
2478 register_timer_api(engine, &active);
2479 register_signal_api(engine, &active);
2480 register_element_ref_api(engine, &active);
2481 register_virtual_collection_api(engine, &active);
2482 (active, renderers)
2483}
2484
2485fn register_render_component_api(
2486 engine: &mut Engine,
2487 exports: &ComponentExportCollector,
2488 renderers: &ComponentRenderRegistry,
2489 active: &ActiveComponentRenderState,
2490) {
2491 let exports = exports.clone();
2492 let renderers = Rc::clone(renderers);
2493 let active = Rc::clone(active);
2494 FuncRegistration::new("render_component")
2495 .in_global_namespace()
2496 .register_into_engine(
2497 engine,
2498 move |call: rhai::NativeCallContext<'_>,
2499 id: ImmutableString,
2500 props: Map|
2501 -> Result<UiNode, Box<EvalAltResult>> {
2502 let id = ModuleId::parse(id.to_string())
2503 .map_err(|error| Box::new(component_render_error(error.to_string())))?;
2504 let generation = active
2505 .try_borrow()
2506 .map_err(|_| {
2507 Box::new(component_render_error(
2508 "component render stack is already borrowed",
2509 ))
2510 })?
2511 .as_ref()
2512 .ok_or_else(|| {
2513 Box::new(component_render_error(
2514 "render_component may run only inside view",
2515 ))
2516 })?
2517 .generation;
2518 let registered = renderers
2519 .try_borrow()
2520 .map_err(|_| {
2521 Box::new(component_render_error(
2522 "component render registry is already borrowed",
2523 ))
2524 })?
2525 .get(&id)
2526 .cloned()
2527 .ok_or_else(|| {
2528 Box::new(component_render_error(format!(
2529 "component `{id}` has no registered render function"
2530 )))
2531 })?;
2532 if registered.generation != generation {
2533 return Err(Box::new(component_render_error(format!(
2534 "component `{id}` render belongs to stale generation {}",
2535 registered.generation
2536 ))));
2537 }
2538 execute_component_render(
2539 &call,
2540 &exports,
2541 &active,
2542 id.as_str().into(),
2543 props,
2544 ®istered.render,
2545 )
2546 },
2547 );
2548}
2549
2550#[allow(clippy::too_many_lines)] fn execute_component_render(
2552 call: &rhai::NativeCallContext<'_>,
2553 exports: &ComponentExportCollector,
2554 active: &ActiveComponentRenderState,
2555 id: ImmutableString,
2556 props: Map,
2557 render: &FnPtr,
2558) -> Result<UiNode, Box<EvalAltResult>> {
2559 let generation = active
2560 .try_borrow()
2561 .map_err(|_| {
2562 Box::new(component_render_error(
2563 "component render stack is already borrowed",
2564 ))
2565 })?
2566 .as_ref()
2567 .ok_or_else(|| {
2568 Box::new(component_render_error(
2569 "formal component rendering may run only inside view",
2570 ))
2571 })?
2572 .generation;
2573 let (component, mut invocation) = resolve_component_invocation(exports, id, props, generation)?;
2574 if render.is_anonymous() || render.is_curried() {
2575 return Err(Box::new(component_render_error(
2576 "formal component render must be an uncurried named function",
2577 )));
2578 }
2579 let part_styles = component_part_styles(&invocation.props);
2580 let recipe_props = invocation.retained_props.clone();
2581 let declared_key = invocation.key.clone();
2582 let render_recipe = render.clone();
2583 let (path, caller_context) = reserve_component_path(call, &component, &invocation, active)?;
2584 if let Some(node) = try_reuse_component_subtree(
2585 active,
2586 &path,
2587 &caller_context,
2588 &component,
2589 &invocation,
2590 render,
2591 )? {
2592 return Ok(node);
2593 }
2594 let native_context = crate::invocation::ScriptInvocationContext::capture_retained(call);
2595 let caller_binding = caller_component_binding(call, &caller_context);
2596 bind_component_callback_props(
2597 &mut invocation.props,
2598 &component.schema.props,
2599 &caller_binding,
2600 );
2601 bind_component_node_props(
2602 &mut invocation.props,
2603 &component.schema.props,
2604 &caller_binding,
2605 );
2606 let event_callbacks = component_event_callbacks(&component, &invocation.props);
2607 let recipe_event_callbacks = event_callbacks.clone();
2608 let script_props = invocation.props.clone();
2609 let context = enter_component_render(
2610 &path,
2611 &component,
2612 &invocation,
2613 native_context.clone(),
2614 active,
2615 )?;
2616 let recipe_component_context = context.clone();
2617 let result = render.call_within_context::<UiNode>(call, (context, invocation.props));
2618 if result.is_err() {
2619 record_component_render_failure(active, &path);
2620 }
2621 register_component_event_callbacks(
2622 active,
2623 &path,
2624 &caller_context,
2625 event_callbacks,
2626 &native_context,
2627 )?;
2628 let (environment, reusable) = component_render_metadata(active, &recipe_component_context)?;
2629 leave_component_render(active)?;
2630 let mut node = result?;
2631 node = node.with_owned_part_styles(part_styles.clone());
2632 let snapshot = crate::node::ComponentOwnedSnapshot::default();
2633 node = node.with_component_root_snapshot(path.clone(), snapshot.reference());
2634 node.bind_component_scope(
2635 &path,
2636 recipe_component_context.component_incarnation(),
2637 &component.schema.events,
2638 Some(&native_context),
2639 );
2640 register_component_invocation(
2641 active,
2642 ComponentInvocationRecipe {
2643 path: path.clone(),
2644 parent: caller_context.component_path().clone(),
2645 component: component.metadata.id.clone(),
2646 export: component.metadata.export.clone(),
2647 declared_key,
2648 props: recipe_props,
2649 script_props,
2650 component_context: recipe_component_context,
2651 caller_context: caller_context.clone(),
2652 part_styles,
2653 event_callbacks: recipe_event_callbacks,
2654 component_events: component.schema.events.clone(),
2655 declared_effects: component.schema.effects.clone(),
2656 render: render_recipe,
2657 snapshot,
2658 context: native_context.clone(),
2659 generation,
2660 environment,
2661 reusable,
2662 },
2663 )?;
2664 Ok(node)
2665}
2666
2667fn caller_component_binding(
2668 call: &rhai::NativeCallContext<'_>,
2669 caller_context: &UiContext,
2670) -> ComponentCallbackBinding {
2671 ComponentCallbackBinding {
2672 component: caller_context.callback_component_path().clone(),
2673 incarnation: caller_context.callback_component_incarnation(),
2674 events: caller_context.callback_event_schemas().clone(),
2675 context: Some(caller_context.native_context().cloned().unwrap_or_else(|| {
2676 crate::invocation::ScriptInvocationContext::capture_entry_retained(call)
2677 })),
2678 }
2679}
2680
2681fn component_render_metadata(
2682 active: &ActiveComponentRenderState,
2683 context: &UiContext,
2684) -> Result<(ComponentRenderEnvironment, bool), Box<EvalAltResult>> {
2685 let guard = active.try_borrow().map_err(|_| {
2686 Box::new(component_render_error(
2687 "component render stack is already borrowed",
2688 ))
2689 })?;
2690 let render = guard.as_ref().ok_or_else(|| {
2691 Box::new(component_render_error(
2692 "formal component rendering may run only inside view",
2693 ))
2694 })?;
2695 Ok((render.environment, context.component_render_is_reusable()))
2696}
2697
2698fn record_component_render_failure(
2699 active: &ActiveComponentRenderState,
2700 component: &ComponentInstancePath,
2701) {
2702 if let Ok(mut active) = active.try_borrow_mut()
2703 && let Some(active) = active.as_mut()
2704 && active.failed_component.is_none()
2705 {
2706 active.failed_component = Some(component.clone());
2707 }
2708}
2709
2710fn try_reuse_component_subtree(
2711 shared: &ActiveComponentRenderState,
2712 path: &ComponentInstancePath,
2713 caller_context: &UiContext,
2714 component: &crate::ComponentDefinition,
2715 invocation: &crate::ComponentInvocation,
2716 render: &FnPtr,
2717) -> Result<Option<UiNode>, Box<EvalAltResult>> {
2718 let mut guard = shared.try_borrow_mut().map_err(|_| {
2719 Box::new(component_render_error(
2720 "component render stack is already borrowed",
2721 ))
2722 })?;
2723 let active = guard.as_mut().ok_or_else(|| {
2724 Box::new(component_render_error(
2725 "formal component rendering may run only inside view",
2726 ))
2727 })?;
2728 let Some(scope) =
2729 reusable_component_scope(active, path, caller_context, component, invocation, render)
2730 else {
2731 return Ok(None);
2732 };
2733
2734 let reused_components = scope.invocations.len();
2735 for candidate in scope.invocations.keys() {
2736 let retained = active.transaction.retain_existing(candidate);
2737 debug_assert!(
2738 retained,
2739 "reuse candidates were checked against the state snapshot"
2740 );
2741 }
2742 active.seen.extend(scope.invocations.keys().cloned());
2743 active.invocations.extend(scope.invocations);
2744 active.event_handlers.extend(scope.event_handlers);
2745 active.effects.extend(scope.resources.effects);
2746 active.timers.extend(scope.resources.timers);
2747 active.signals.extend(scope.resources.signals);
2748 active.element_refs.extend(scope.resources.element_refs);
2749 active
2750 .virtual_collections
2751 .extend(scope.resources.virtual_collections);
2752 active
2753 .reused
2754 .push((component.metadata.id.to_string(), reused_components));
2755 Ok(Some(scope.node))
2756}
2757
2758fn prune_virtual_read_contributions(
2759 context: &UiContext,
2760 id: &crate::VirtualCollectionId,
2761 data: &crate::VirtualCollectionData,
2762 indices: &BTreeSet<usize>,
2763 retained: &UiNode,
2764) -> Result<(), RuntimeError> {
2765 let mut active = retained.virtual_read_contributions();
2766 active.extend(
2767 indices
2768 .iter()
2769 .filter_map(|index| data.key(*index))
2770 .map(
2771 |key| crate::read_dependency::ReadContribution::VirtualItem {
2772 collection: id.clone(),
2773 key: key.to_owned(),
2774 },
2775 ),
2776 );
2777 context
2778 .runtime()
2779 .try_borrow_mut()
2780 .map_err(|_| RuntimeError::ComponentRuntime("UI state is already borrowed".into()))?
2781 .retain_virtual_read_contributions(context.component_path(), &active);
2782 Ok(())
2783}
2784
2785fn retain_virtual_component_manifest(
2786 shared: &ActiveComponentRenderState,
2787) -> Result<(), RuntimeError> {
2788 let mut guard = shared.try_borrow_mut().map_err(|_| {
2789 RuntimeError::ComponentRuntime("component render stack is already borrowed".to_owned())
2790 })?;
2791 let active = guard.as_mut().ok_or_else(|| {
2792 RuntimeError::ComponentRuntime("virtual render has no active transaction".to_owned())
2793 })?;
2794 let Some(reuse) = active.reuse.as_ref() else {
2795 return Ok(());
2796 };
2797 let mut retained = reuse
2798 .invocations
2799 .keys()
2800 .filter(|path| reuse.subtrees.contains(path))
2801 .cloned()
2802 .collect::<BTreeSet<_>>();
2803 let mut children = BTreeMap::<ComponentInstancePath, Vec<ComponentInstancePath>>::new();
2806 for (path, recipe) in &reuse.invocations {
2807 children
2808 .entry(recipe.parent.clone())
2809 .or_default()
2810 .push(path.clone());
2811 }
2812 let mut pending = retained.iter().cloned().collect::<Vec<_>>();
2813 while let Some(parent) = pending.pop() {
2814 for child in children.get(&parent).into_iter().flatten() {
2815 if retained.insert(child.clone()) {
2816 pending.push(child.clone());
2817 }
2818 }
2819 }
2820 for path in &retained {
2821 if !active.transaction.retain_existing(path) {
2822 return Err(RuntimeError::ComponentRuntime(format!(
2823 "retained virtual component `{path}` has no committed state"
2824 )));
2825 }
2826 }
2827 active.seen.extend(retained.iter().cloned());
2828 active.invocations.extend(
2829 reuse
2830 .invocations
2831 .iter()
2832 .filter(|(path, _)| retained.contains(*path))
2833 .map(|(path, recipe)| (path.clone(), recipe.clone())),
2834 );
2835 active.event_handlers.extend(
2836 reuse
2837 .event_handlers
2838 .iter()
2839 .filter(|((path, _), _)| retained.contains(path))
2840 .map(|(id, callback)| (id.clone(), callback.clone())),
2841 );
2842 active.effects.extend(
2843 reuse
2844 .effects
2845 .iter()
2846 .filter(|(id, _)| retained.contains(id.component()))
2847 .map(|(id, descriptor)| (id.clone(), descriptor.clone())),
2848 );
2849 active.timers.extend(
2850 reuse
2851 .timers
2852 .iter()
2853 .filter(|(id, _)| retained.contains(id.component()))
2854 .map(|(id, descriptor)| (id.clone(), descriptor.clone())),
2855 );
2856 active.signals.extend(
2857 reuse
2858 .signals
2859 .iter()
2860 .filter(|(id, _)| retained.contains(id.component()))
2861 .map(|(id, descriptor)| (id.clone(), descriptor.clone())),
2862 );
2863 active.element_refs.extend(
2864 reuse
2865 .element_refs
2866 .iter()
2867 .filter(|id| retained.contains(id.component()))
2868 .cloned(),
2869 );
2870 active.virtual_collections.extend(
2871 reuse
2872 .virtual_collections
2873 .iter()
2874 .filter(|(id, _)| reuse.previous_root.virtual_collection_spec(id).is_some())
2875 .map(|(id, recipe)| (id.clone(), recipe.clone())),
2876 );
2877 Ok(())
2878}
2879
2880fn reusable_component_scope(
2881 active: &ActiveComponentRender,
2882 path: &ComponentInstancePath,
2883 caller_context: &UiContext,
2884 component: &crate::ComponentDefinition,
2885 invocation: &crate::ComponentInvocation,
2886 render: &FnPtr,
2887) -> Option<ReusedComponentScope> {
2888 let reuse = active.reuse.as_ref()?;
2889 if reuse.dirty.iter().any(|dirty| dirty.is_within(path)) {
2890 return None;
2891 }
2892 let recipe = reuse.invocations.get(path)?;
2893 if !recipe.reusable
2894 || recipe.generation != active.generation
2895 || recipe.environment != active.environment
2896 || recipe.parent != *caller_context.component_path()
2897 || recipe.component != component.metadata.id
2898 || recipe.export != component.metadata.export
2899 || recipe.declared_key != invocation.key
2900 || recipe.render.fn_name() != render.fn_name()
2901 || !recipe.props.reusable_eq(&invocation.retained_props)
2902 {
2903 return None;
2904 }
2905 let invocations = reuse
2906 .invocations
2907 .range(path.clone()..)
2908 .take_while(|(candidate, _)| candidate.is_within(path))
2909 .map(|(candidate, recipe)| (candidate.clone(), recipe.clone()))
2910 .collect::<BTreeMap<_, _>>();
2911 if invocations.is_empty()
2912 || invocations.values().any(|recipe| !recipe.reusable)
2913 || invocations
2914 .keys()
2915 .any(|candidate| !active.state_snapshot.contains_instance(candidate))
2916 {
2917 return None;
2918 }
2919 let node = if let Some(snapshot) = recipe.snapshot.current() {
2920 let mut node = snapshot.as_ref().clone();
2921 node.hydrate_component_subtrees();
2922 node
2923 } else {
2924 reuse.subtrees.get(&reuse.previous_root, path)?.clone()
2925 };
2926 Some(ReusedComponentScope {
2927 node,
2928 invocations,
2929 event_handlers: reuse
2930 .event_handlers
2931 .range((path.clone(), String::new())..)
2932 .take_while(|((candidate, _), _)| candidate.is_within(path))
2933 .map(|(id, callback)| (id.clone(), callback.clone()))
2934 .collect(),
2935 resources: reused_component_resources(reuse, path),
2936 })
2937}
2938
2939fn reused_component_resources(
2940 reuse: &ComponentReuseSnapshot,
2941 path: &ComponentInstancePath,
2942) -> ReusedComponentResources {
2943 ReusedComponentResources {
2944 effects: reuse
2945 .effects
2946 .iter()
2947 .filter(|(id, _)| id.component().is_within(path))
2948 .map(|(id, descriptor)| (id.clone(), descriptor.clone()))
2949 .collect(),
2950 timers: reuse
2951 .timers
2952 .iter()
2953 .filter(|(id, _)| id.component().is_within(path))
2954 .map(|(id, descriptor)| (id.clone(), descriptor.clone()))
2955 .collect(),
2956 signals: reuse
2957 .signals
2958 .iter()
2959 .filter(|(id, _)| id.component().is_within(path))
2960 .map(|(id, descriptor)| (id.clone(), descriptor.clone()))
2961 .collect(),
2962 element_refs: reuse
2963 .element_refs
2964 .iter()
2965 .filter(|id| id.component().is_within(path))
2966 .cloned()
2967 .collect(),
2968 virtual_collections: reuse
2969 .virtual_collections
2970 .iter()
2971 .filter(|(id, _)| id.component.is_within(path))
2972 .map(|(id, recipe)| (id.clone(), recipe.clone()))
2973 .collect(),
2974 }
2975}
2976
2977fn register_component_invocation(
2978 active: &ActiveComponentRenderState,
2979 recipe: ComponentInvocationRecipe,
2980) -> Result<(), Box<EvalAltResult>> {
2981 let mut guard = active.try_borrow_mut().map_err(|_| {
2982 Box::new(component_render_error(
2983 "component render stack is already borrowed",
2984 ))
2985 })?;
2986 let active = guard.as_mut().ok_or_else(|| {
2987 Box::new(component_render_error(
2988 "formal component rendering may run only inside view",
2989 ))
2990 })?;
2991 active.invocations.insert(recipe.path.clone(), recipe);
2992 Ok(())
2993}
2994
2995fn resolve_component_invocation(
2996 exports: &ComponentExportCollector,
2997 id: ImmutableString,
2998 props: Map,
2999 generation: ScriptGeneration,
3000) -> Result<(crate::ComponentDefinition, crate::ComponentInvocation), Box<EvalAltResult>> {
3001 let id: String = id.into();
3002 let id =
3003 ModuleId::parse(id).map_err(|error| Box::new(component_render_error(error.to_string())))?;
3004 let registry = exports
3005 .snapshot()
3006 .map_err(|error| Box::new(component_render_error(error.to_string())))?;
3007 let component = registry.get(&id).cloned().ok_or_else(|| {
3008 Box::new(component_render_error(format!(
3009 "component `{id}` is not exported"
3010 )))
3011 })?;
3012 let key = props.get("key").and_then(|value| {
3013 value
3014 .is::<ImmutableString>()
3015 .then(|| value.clone_cast::<ImmutableString>().to_string())
3016 });
3017 let invocation = component
3018 .invoke_with_generation(key, props, generation)
3019 .map_err(|error| Box::new(component_render_error(error.to_string())))?;
3020 Ok((component, invocation))
3021}
3022
3023fn reserve_component_path(
3024 call: &rhai::NativeCallContext<'_>,
3025 component: &crate::ComponentDefinition,
3026 invocation: &crate::ComponentInvocation,
3027 shared: &ActiveComponentRenderState,
3028) -> Result<(ComponentInstancePath, UiContext), Box<EvalAltResult>> {
3029 let mut guard = shared.try_borrow_mut().map_err(|_| {
3030 Box::new(component_render_error(
3031 "component render stack is already borrowed",
3032 ))
3033 })?;
3034 let active = guard.as_mut().ok_or_else(|| {
3035 Box::new(component_render_error(
3036 "formal component rendering may run only inside view",
3037 ))
3038 })?;
3039 let caller_context = active.contexts.last().cloned().ok_or_else(|| {
3040 Box::new(component_render_error(
3041 "component render context stack has no caller",
3042 ))
3043 })?;
3044 let parent = active.stack.last().cloned().ok_or_else(|| {
3045 Box::new(component_render_error(
3046 "component render stack has no application root",
3047 ))
3048 })?;
3049 let key = invocation.key.clone().unwrap_or_else(|| {
3050 let callsite = format!(
3051 "{}@{}",
3052 call.call_source().unwrap_or("<script>"),
3053 call.call_position()
3054 );
3055 let occurrence = active
3056 .positions
3057 .entry(format!("{parent}/{}/{callsite}", component.metadata.export))
3058 .or_default();
3059 let key = format!("{callsite}#{occurrence}");
3060 *occurrence = occurrence.saturating_add(1);
3061 key
3062 });
3063 let path = parent.child(&component.metadata.export, key);
3064 if !active.seen.insert(path.clone()) {
3065 return Err(Box::new(component_render_error(format!(
3066 "duplicate component instance path `{path}`; add a stable key"
3067 ))));
3068 }
3069 Ok((path, caller_context))
3070}
3071
3072fn enter_component_render(
3073 path: &ComponentInstancePath,
3074 component: &crate::ComponentDefinition,
3075 invocation: &crate::ComponentInvocation,
3076 native_context: crate::invocation::ScriptInvocationContext,
3077 shared: &ActiveComponentRenderState,
3078) -> Result<UiContext, Box<EvalAltResult>> {
3079 let mut guard = shared.try_borrow_mut().map_err(|_| {
3080 Box::new(component_render_error(
3081 "component render stack is already borrowed",
3082 ))
3083 })?;
3084 let active = guard.as_mut().ok_or_else(|| {
3085 Box::new(component_render_error(
3086 "formal component rendering may run only inside view",
3087 ))
3088 })?;
3089 active
3090 .transaction
3091 .mount(path.clone(), &component.schema.state)
3092 .map_err(|error| Box::new(component_render_error(error.to_string())))?;
3093 let global_part_styles = {
3094 let mut runtime = active
3095 .root_context
3096 .runtime()
3097 .try_borrow_mut()
3098 .map_err(|_| Box::new(component_render_error("UI state is already borrowed")))?;
3099 runtime
3100 .component_state
3101 .mount_instance(path.clone(), &component.schema.state)
3102 .map_err(|error| Box::new(component_render_error(error.to_string())))?;
3103 runtime
3104 .component_styles()
3105 .component(&component.metadata.id)
3106 .cloned()
3107 .unwrap_or_default()
3108 };
3109 let context = active
3110 .root_context
3111 .for_component(path.clone(), component.schema.events.clone())
3112 .with_component_styles(
3113 global_part_styles,
3114 component_root_style(&invocation.props),
3115 component_part_styles(&invocation.props),
3116 )
3117 .with_native_context(Some(native_context))
3118 .with_generation(active.generation);
3119 active.stack.push(path.clone());
3120 active.contexts.push(context.clone());
3121 active
3122 .effect_keys
3123 .push(Some(component.schema.effects.clone()));
3124 Ok(context)
3125}
3126
3127fn leave_component_render(active: &ActiveComponentRenderState) -> Result<(), Box<EvalAltResult>> {
3128 let mut guard = active.try_borrow_mut().map_err(|_| {
3129 Box::new(component_render_error(
3130 "component render stack is already borrowed",
3131 ))
3132 })?;
3133 if let Some(active) = guard.as_mut() {
3134 active.stack.pop();
3135 active.contexts.pop();
3136 active.effect_keys.pop();
3137 }
3138 Ok(())
3139}
3140
3141fn register_effect_api(engine: &mut Engine, active: &ActiveComponentRenderState) {
3142 let active = Rc::clone(active);
3143 FuncRegistration::new("effect")
3144 .in_global_namespace()
3145 .register_into_engine(
3146 engine,
3147 move |call: rhai::NativeCallContext<'_>,
3148 key: ImmutableString,
3149 dependencies: Dynamic,
3150 start: FnPtr,
3151 cleanup: FnPtr|
3152 -> Result<(), Box<EvalAltResult>> {
3153 let key = key.to_string();
3154 let (component, context, generation, declared) = {
3155 let guard = active.try_borrow().map_err(|_| {
3156 Box::new(component_render_error(
3157 "component render stack is already borrowed",
3158 ))
3159 })?;
3160 let render = guard.as_ref().ok_or_else(|| {
3161 Box::new(component_render_error(
3162 "effect may run only during formal component render",
3163 ))
3164 })?;
3165 let context = render.contexts.last().cloned().ok_or_else(|| {
3166 Box::new(component_render_error(
3167 "component render context stack is empty",
3168 ))
3169 })?;
3170 let declared =
3171 render
3172 .effect_keys
3173 .last()
3174 .cloned()
3175 .flatten()
3176 .ok_or_else(|| {
3177 Box::new(component_render_error(
3178 "effects may be declared only by formal components",
3179 ))
3180 })?;
3181 (
3182 context.component_path().clone(),
3183 context,
3184 render.generation,
3185 declared,
3186 )
3187 };
3188 if !declared.contains(&key) {
3189 return Err(Box::new(component_render_error(
3190 crate::EffectError::Undeclared { component, key }.to_string(),
3191 )));
3192 }
3193 let id = crate::EffectId::new(component.clone(), key.clone())
3194 .map_err(|error| Box::new(component_render_error(error.to_string())))?;
3195 let dependencies = UiValue::from_dynamic(dependencies)
3196 .map_err(|error| Box::new(component_render_error(error.to_string())))?;
3197 let native_context =
3198 crate::invocation::ScriptInvocationContext::capture_retained(&call);
3199 let mut start = ScriptCallback::try_from_fn_ptr(start, generation)
3200 .map_err(|error| Box::new(component_render_error(error.to_string())))?;
3201 start.bind_component_scope_if_unset(
3202 &component,
3203 context.component_incarnation(),
3204 context.event_schemas().clone(),
3205 );
3206 start.bind_native_context_if_unset(native_context.clone());
3207 let mut cleanup = ScriptCallback::try_from_fn_ptr(cleanup, generation)
3208 .map_err(|error| Box::new(component_render_error(error.to_string())))?;
3209 cleanup.bind_component_scope_if_unset(
3210 &component,
3211 context.component_incarnation(),
3212 context.event_schemas().clone(),
3213 );
3214 cleanup.bind_native_context_if_unset(native_context);
3215 let descriptor =
3216 crate::EffectDescriptor::new(id.clone(), dependencies, start, cleanup);
3217 let mut guard = active.try_borrow_mut().map_err(|_| {
3218 Box::new(component_render_error(
3219 "component render stack is already borrowed",
3220 ))
3221 })?;
3222 let render = guard.as_mut().ok_or_else(|| {
3223 Box::new(component_render_error(
3224 "effect may run only during formal component render",
3225 ))
3226 })?;
3227 if render.effects.insert(id, descriptor).is_some() {
3228 return Err(Box::new(component_render_error(
3229 crate::EffectError::Duplicate { component, key }.to_string(),
3230 )));
3231 }
3232 Ok(())
3233 },
3234 );
3235}
3236
3237fn register_timer_api(engine: &mut Engine, active: &ActiveComponentRenderState) {
3238 let active = Rc::clone(active);
3239 FuncRegistration::new("timeout")
3240 .in_global_namespace()
3241 .register_into_engine(
3242 engine,
3243 move |call: rhai::NativeCallContext<'_>,
3244 key: ImmutableString,
3245 delay_ms: rhai::INT,
3246 paused: bool,
3247 callback: FnPtr,
3248 payload: Dynamic|
3249 -> Result<(), Box<EvalAltResult>> {
3250 let (component, context, generation) = {
3251 let guard = active.try_borrow().map_err(|_| {
3252 Box::new(component_render_error(
3253 "component render stack is already borrowed",
3254 ))
3255 })?;
3256 let render = guard.as_ref().ok_or_else(|| {
3257 Box::new(component_render_error(
3258 "timeout may run only during formal component render",
3259 ))
3260 })?;
3261 let context = render.contexts.last().cloned().ok_or_else(|| {
3262 Box::new(component_render_error(
3263 "component render context stack is empty",
3264 ))
3265 })?;
3266 (context.component_path().clone(), context, render.generation)
3267 };
3268 let delay_ms = u64::try_from(delay_ms).map_err(|_| {
3269 Box::new(component_render_error(
3270 "timeout delay must be a positive integer",
3271 ))
3272 })?;
3273 let id = crate::TimerId::new(component.clone(), key.to_string())
3274 .map_err(|error| Box::new(component_render_error(error.to_string())))?;
3275 let payload = UiValue::from_dynamic(payload)
3276 .map_err(|error| Box::new(component_render_error(error.to_string())))?;
3277 let native_context =
3278 crate::invocation::ScriptInvocationContext::capture_retained(&call);
3279 let mut callback = ScriptCallback::try_from_fn_ptr(callback, generation)
3280 .map_err(|error| Box::new(component_render_error(error.to_string())))?;
3281 callback.bind_component_scope_if_unset(
3282 &component,
3283 context.component_incarnation(),
3284 context.event_schemas().clone(),
3285 );
3286 callback.bind_native_context_if_unset(native_context);
3287 let descriptor = crate::TimerDescriptor::new(
3288 id.clone(),
3289 Duration::from_millis(delay_ms),
3290 paused,
3291 callback,
3292 payload,
3293 )
3294 .map_err(|error| Box::new(component_render_error(error.to_string())))?;
3295 let mut guard = active.try_borrow_mut().map_err(|_| {
3296 Box::new(component_render_error(
3297 "component render stack is already borrowed",
3298 ))
3299 })?;
3300 let render = guard.as_mut().ok_or_else(|| {
3301 Box::new(component_render_error(
3302 "timeout may run only during formal component render",
3303 ))
3304 })?;
3305 if render.timers.insert(id.clone(), descriptor).is_some() {
3306 return Err(Box::new(component_render_error(format!(
3307 "timeout `{}` is declared more than once in `{}`",
3308 id.key(),
3309 id.component()
3310 ))));
3311 }
3312 Ok(())
3313 },
3314 );
3315}
3316
3317fn register_signal_api(engine: &mut Engine, active: &ActiveComponentRenderState) {
3318 let scalar_active = Rc::clone(active);
3319 FuncRegistration::new("signal")
3320 .in_global_namespace()
3321 .register_into_engine(
3322 engine,
3323 move |key: ImmutableString,
3324 initial: Dynamic|
3325 -> Result<crate::NativeSignal, Box<EvalAltResult>> {
3326 let key = key.to_string();
3327 let initial = crate::SignalValue::from_dynamic(initial)
3328 .map_err(|error| Box::new(crate::signal::signal_runtime_error(&error)))?;
3329 declare_signal(&scalar_active, key, initial)
3330 },
3331 );
3332
3333 let optional_active = Rc::clone(active);
3334 FuncRegistration::new("optional_float_signal")
3335 .in_global_namespace()
3336 .register_into_engine(
3337 engine,
3338 move |key: ImmutableString| -> Result<crate::NativeSignal, Box<EvalAltResult>> {
3339 declare_signal(
3340 &optional_active,
3341 key.to_string(),
3342 crate::SignalValue::OptionalFloat(None),
3343 )
3344 },
3345 );
3346}
3347
3348fn declare_signal(
3349 active: &ActiveComponentRenderState,
3350 key: String,
3351 initial: crate::SignalValue,
3352) -> Result<crate::NativeSignal, Box<EvalAltResult>> {
3353 let mut guard = active.try_borrow_mut().map_err(|_| {
3354 Box::new(crate::signal::signal_runtime_error(
3355 &"component render stack is already borrowed",
3356 ))
3357 })?;
3358 let render = guard.as_mut().ok_or_else(|| {
3359 Box::new(crate::signal::signal_runtime_error(
3360 &"signal may run only during formal component render",
3361 ))
3362 })?;
3363 if render.effect_keys.last().and_then(Option::as_ref).is_none() {
3364 return Err(Box::new(crate::signal::signal_runtime_error(
3365 &"signals may be declared only by formal components",
3366 )));
3367 }
3368 let context = render.contexts.last().ok_or_else(|| {
3369 Box::new(crate::signal::signal_runtime_error(
3370 &"component render context stack is empty",
3371 ))
3372 })?;
3373 let component = context.component_path().clone();
3374 let incarnation = context.component_incarnation();
3375 if render
3376 .signals
3377 .keys()
3378 .any(|id| id.component() == &component && id.key() == key)
3379 {
3380 return Err(Box::new(crate::signal::signal_runtime_error(
3381 &crate::SignalError::Duplicate { component, key },
3382 )));
3383 }
3384 let id = crate::SignalId::new_scoped(component, incarnation, key, initial.kind())
3385 .map_err(|error| Box::new(crate::signal::signal_runtime_error(&error)))?;
3386 let signal = crate::NativeSignal::new(id.clone());
3387 render
3388 .signals
3389 .insert(id, crate::signal::SignalDescriptor::new(initial));
3390 Ok(signal)
3391}
3392
3393fn register_element_ref_api(engine: &mut Engine, active: &ActiveComponentRenderState) {
3394 let active = Rc::clone(active);
3395 FuncRegistration::new("element_ref")
3396 .in_global_namespace()
3397 .register_into_engine(
3398 engine,
3399 move |key: ImmutableString| -> Result<crate::ElementRef, Box<EvalAltResult>> {
3400 let key = key.to_string();
3401 let mut guard = active.try_borrow_mut().map_err(|_| {
3402 Box::new(component_render_error(
3403 "component render stack is already borrowed",
3404 ))
3405 })?;
3406 let render = guard.as_mut().ok_or_else(|| {
3407 Box::new(component_render_error(
3408 "element_ref may run only during formal component render",
3409 ))
3410 })?;
3411 if render.effect_keys.last().and_then(Option::as_ref).is_none() {
3412 return Err(Box::new(component_render_error(
3413 "element refs may be declared only by formal components",
3414 )));
3415 }
3416 let component = render
3417 .contexts
3418 .last()
3419 .ok_or_else(|| {
3420 Box::new(component_render_error(
3421 "component render context stack is empty",
3422 ))
3423 })?
3424 .component_path()
3425 .clone();
3426 let id = crate::ElementRefId::new(component.clone(), key.clone())
3427 .map_err(|error| Box::new(component_render_error(error.to_string())))?;
3428 if !render.element_refs.insert(id.clone()) {
3429 return Err(Box::new(component_render_error(format!(
3430 "element ref `{key}` is declared more than once by component `{component}`"
3431 ))));
3432 }
3433 Ok(crate::ElementRef::new(id))
3434 },
3435 );
3436}
3437
3438#[derive(Debug)]
3439struct DecodedVirtualCollection {
3440 key: String,
3441 label: String,
3442 data: crate::VirtualCollectionData,
3443 estimated_height: f64,
3444 height: Option<f64>,
3445 overdraw_pixels: f64,
3446 bottom_align: bool,
3447 follow_tail: bool,
3448 reveal_key: Option<String>,
3449 sticky_headers: Arc<BTreeSet<usize>>,
3450}
3451
3452impl DecodedVirtualCollection {
3453 fn into_spec(
3454 self,
3455 id: crate::VirtualCollectionId,
3456 realized: BTreeMap<usize, UiNode>,
3457 ) -> crate::VirtualCollectionNodeSpec {
3458 crate::VirtualCollectionNodeSpec {
3459 id,
3460 label: self.label,
3461 data: self.data,
3462 realized,
3463 estimated_height: self.estimated_height,
3464 height: self.height,
3465 overdraw_pixels: self.overdraw_pixels,
3466 bottom_align: self.bottom_align,
3467 follow_tail: self.follow_tail,
3468 reveal_key: self.reveal_key,
3469 sticky_headers: self.sticky_headers,
3470 inherited_motion_group: None,
3471 }
3472 }
3473}
3474
3475fn decode_virtual_collection(
3476 mut config: Map,
3477) -> Result<DecodedVirtualCollection, Box<EvalAltResult>> {
3478 let key = collection_string(&mut config, "key")?;
3479 let label = collection_optional_string(&mut config, "label")?.unwrap_or_default();
3480 let estimated_height = collection_positive(&mut config, "estimated_height")?;
3481 let height = collection_optional_number(&mut config, "height")?;
3482 let fill_height = collection_optional_bool(&mut config, "fill_height")?.unwrap_or(false);
3483 let height = match (height, fill_height) {
3484 (Some(_), true) => {
3485 return Err(Box::new(component_render_error(
3486 "virtual collection must specify either `height` or `fill_height`, not both",
3487 )));
3488 }
3489 (None, false) => {
3490 return Err(Box::new(component_render_error(
3491 "virtual collection requires `height` or `fill_height: true`",
3492 )));
3493 }
3494 (None, true) => None,
3495 (Some(height), false) if height.is_finite() && height > 0.0 => Some(height),
3496 (Some(_), false) => {
3497 return Err(Box::new(component_render_error(
3498 "virtual collection `height` must be finite and positive",
3499 )));
3500 }
3501 };
3502 let overdraw_pixels = collection_optional_number(&mut config, "overdraw_pixels")?
3503 .unwrap_or(estimated_height * 2.0);
3504 if !overdraw_pixels.is_finite() || !(0.0..=10_000.0).contains(&overdraw_pixels) {
3505 return Err(Box::new(component_render_error(
3506 "virtual collection overdraw_pixels must be between 0 and 10000",
3507 )));
3508 }
3509 let alignment =
3510 collection_optional_string(&mut config, "alignment")?.unwrap_or_else(|| "top".to_owned());
3511 let bottom_align = match alignment.as_str() {
3512 "top" => false,
3513 "bottom" => true,
3514 _ => {
3515 return Err(Box::new(component_render_error(
3516 "virtual collection alignment must be `top` or `bottom`",
3517 )));
3518 }
3519 };
3520 let follow_tail = collection_optional_bool(&mut config, "follow_tail")?.unwrap_or(false);
3521 let data = collection_data(&mut config)?;
3522 let reveal_key = collection_optional_string(&mut config, "reveal_key")?;
3523 if reveal_key.as_deref() == Some("") {
3524 return Err(Box::new(component_render_error(
3525 "virtual collection `reveal_key` must not be empty",
3526 )));
3527 }
3528 if let Some(reveal_key) = reveal_key.as_deref()
3529 && !(0..data.len()).any(|index| data.key(index) == Some(reveal_key))
3530 {
3531 return Err(Box::new(component_render_error(format!(
3532 "virtual collection reveal key `{reveal_key}` is not present in its data"
3533 ))));
3534 }
3535 if follow_tail && reveal_key.is_some() {
3536 return Err(Box::new(component_render_error(
3537 "virtual collection must not combine `follow_tail` with `reveal_key`",
3538 )));
3539 }
3540 let sticky_headers = collection_optional_indices(&mut config, "sticky_headers")?
3541 .map_or_else(|| data.sticky_headers(), Arc::new);
3542 if let Some(index) = sticky_headers.iter().find(|index| **index >= data.len()) {
3543 return Err(Box::new(component_render_error(format!(
3544 "virtual collection sticky header index {index} is outside {} items",
3545 data.len()
3546 ))));
3547 }
3548 if bottom_align && !sticky_headers.is_empty() {
3549 return Err(Box::new(component_render_error(
3550 "sticky headers require top-aligned virtual collections",
3551 )));
3552 }
3553 if let Some((unknown, _)) = config.into_iter().next() {
3554 return Err(Box::new(component_render_error(format!(
3555 "unknown virtual collection field `{unknown}`"
3556 ))));
3557 }
3558 Ok(DecodedVirtualCollection {
3559 key,
3560 label,
3561 data,
3562 estimated_height,
3563 height,
3564 overdraw_pixels,
3565 bottom_align,
3566 follow_tail,
3567 reveal_key,
3568 sticky_headers,
3569 })
3570}
3571
3572fn register_virtual_collection_api(engine: &mut Engine, active: &ActiveComponentRenderState) {
3573 let active = Rc::clone(active);
3574 FuncRegistration::new("virtual_collection")
3575 .in_global_namespace()
3576 .register_into_engine(
3577 engine,
3578 move |call: rhai::NativeCallContext<'_>,
3579 config: Map,
3580 renderer: FnPtr|
3581 -> Result<UiNode, Box<EvalAltResult>> {
3582 validate_virtual_renderer(&renderer)?;
3583 let decoded = decode_virtual_collection(config)?;
3584
3585 let VirtualCollectionContext {
3586 component,
3587 context,
3588 generation,
3589 } = virtual_collection_context(&active)?;
3590 let id = crate::VirtualCollectionId {
3591 component: component.clone(),
3592 key: decoded.key.clone(),
3593 };
3594 let collection_context = context
3595 .for_structural_scope(
3596 component.child("VirtualCollection", decoded.key.clone()),
3597 BTreeMap::new(),
3598 )
3599 .with_generation(generation);
3600 let native_context =
3601 crate::invocation::ScriptInvocationContext::capture_retained(&call);
3602 let realized = realize_seeded_virtual_collection(
3603 &call,
3604 &renderer,
3605 &collection_context,
3606 &active,
3607 &id,
3608 &decoded,
3609 )?;
3610 let recipe = VirtualCollectionRecipe {
3611 id: id.clone(),
3612 data: decoded.data.clone(),
3613 renderer,
3614 renderer_context: native_context,
3615 context: collection_context,
3616 generation,
3617 };
3618 let mut guard = active.try_borrow_mut().map_err(|_| {
3619 Box::new(component_render_error(
3620 "component render stack is already borrowed",
3621 ))
3622 })?;
3623 let render = guard.as_mut().ok_or_else(|| {
3624 Box::new(component_render_error(
3625 "virtual_collection may run only during view render",
3626 ))
3627 })?;
3628 if render
3629 .virtual_collections
3630 .insert(id.clone(), recipe)
3631 .is_some()
3632 {
3633 return Err(Box::new(component_render_error(format!(
3634 "virtual collection `{}` is declared more than once in `{}`",
3635 decoded.key, id.component
3636 ))));
3637 }
3638 Ok(UiNode::virtual_collection(decoded.into_spec(id, realized)))
3639 },
3640 );
3641}
3642
3643fn validate_virtual_renderer(renderer: &FnPtr) -> Result<(), Box<EvalAltResult>> {
3644 if renderer.is_anonymous() {
3645 return Err(Box::new(component_render_error(
3646 "virtual collection item renderer must be a named function",
3647 )));
3648 }
3649 for (index, value) in renderer.iter_curry().enumerate() {
3650 validate_virtual_renderer_curry(value).map_err(|error| {
3651 Box::new(component_render_error(format!(
3652 "virtual collection renderer curry {index} is not durable presentation data: {error}"
3653 )))
3654 })?;
3655 }
3656 Ok(())
3657}
3658
3659fn validate_virtual_renderer_curry(value: &Dynamic) -> Result<(), crate::UiValueError> {
3660 if value.is::<UiNode>() || value.is::<crate::NativeSignal>() {
3661 return Ok(());
3662 }
3663 if value.is::<Array>() {
3664 for value in value.clone_cast::<Array>() {
3665 validate_virtual_renderer_curry(&value)?;
3666 }
3667 return Ok(());
3668 }
3669 if value.is::<Map>() {
3670 for value in value.clone_cast::<Map>().values() {
3671 validate_virtual_renderer_curry(value)?;
3672 }
3673 return Ok(());
3674 }
3675 UiValue::from_dynamic(value.clone()).map(|_| ())
3676}
3677
3678fn realize_initial_collection(
3679 call: &rhai::NativeCallContext<'_>,
3680 renderer: &FnPtr,
3681 context: &UiContext,
3682 id: &crate::VirtualCollectionId,
3683 data: &crate::VirtualCollectionData,
3684 indices: &BTreeSet<usize>,
3685) -> Result<BTreeMap<usize, UiNode>, Box<EvalAltResult>> {
3686 let mut realized = BTreeMap::new();
3687 for index in indices.iter().copied().filter(|index| *index < data.len()) {
3688 let item = data
3689 .item(index)
3690 .map_err(|error| Box::new(component_render_error(error.to_string())))?
3691 .ok_or_else(|| {
3692 Box::new(component_render_error(format!(
3693 "virtual collection item {index} disappeared"
3694 )))
3695 })?;
3696 let (item_key, mut payload) = collection_payload(&item, index)?;
3697 add_collection_neighbors(data, index, &mut payload);
3698 let node = renderer
3699 .call_within_context::<UiNode>(
3700 call,
3701 (context.for_virtual_item(id, &item_key), payload),
3702 )?
3703 .with_key(item_key);
3704 realized.insert(index, node);
3705 }
3706 Ok(realized)
3707}
3708
3709fn realize_seeded_virtual_collection(
3710 call: &rhai::NativeCallContext<'_>,
3711 renderer: &FnPtr,
3712 context: &UiContext,
3713 active: &ActiveComponentRenderState,
3714 id: &crate::VirtualCollectionId,
3715 decoded: &DecodedVirtualCollection,
3716) -> Result<BTreeMap<usize, UiNode>, Box<EvalAltResult>> {
3717 let previous_metrics = context
3718 .runtime()
3719 .try_borrow()
3720 .map_err(|_| {
3721 Box::new(component_render_error(
3722 "UI state is already borrowed while seeding a virtual collection",
3723 ))
3724 })?
3725 .virtual_requests
3726 .metrics(id);
3727 let viewport = decoded.height.unwrap_or_else(|| {
3728 previous_metrics
3729 .as_ref()
3730 .map_or(decoded.estimated_height, |metrics| {
3731 if metrics.viewport_height > 0.0 {
3732 metrics.viewport_height
3733 } else {
3734 decoded.estimated_height
3735 }
3736 })
3737 });
3738 let count = nonnegative_usize(
3739 ((viewport + decoded.overdraw_pixels) / decoded.estimated_height).ceil() + 1.0,
3740 )
3741 .min(decoded.data.len());
3742 let indices =
3743 virtual_collection_seed_indices(active, id, decoded, count, previous_metrics.as_ref())?;
3744 enter_virtual_collection_scope(active, context.clone())?;
3745 let realized = realize_initial_collection(call, renderer, context, id, &decoded.data, &indices);
3746 leave_component_render(active)?;
3747 realized
3748}
3749
3750fn virtual_collection_seed_indices(
3751 active: &ActiveComponentRenderState,
3752 id: &crate::VirtualCollectionId,
3753 decoded: &DecodedVirtualCollection,
3754 count: usize,
3755 metrics: Option<&crate::VirtualCollectionMetrics>,
3756) -> Result<BTreeSet<usize>, Box<EvalAltResult>> {
3757 let mut indices = BTreeSet::new();
3758 let guard = active.try_borrow().map_err(|_| {
3759 Box::new(component_render_error(
3760 "component render stack is already borrowed",
3761 ))
3762 })?;
3763 let reuse = guard.as_ref().and_then(|render| render.reuse.as_ref());
3764 let previous = reuse.and_then(|reuse| reuse.previous_root.virtual_collection_spec(id));
3765 let preserves_scroll = previous.is_some_and(|previous| {
3766 previous.estimated_height.to_bits() == decoded.estimated_height.to_bits()
3767 && previous.overdraw_pixels.to_bits() == decoded.overdraw_pixels.to_bits()
3768 && previous.bottom_align == decoded.bottom_align
3769 && same_collection_key_order(&previous.data, &decoded.data)
3770 });
3771
3772 if preserves_scroll {
3773 if let Some(previous) = previous {
3774 indices.extend(previous.realized.keys().copied());
3775 }
3776 if let Some(metrics) = metrics {
3777 extend_virtual_window(
3778 &mut indices,
3779 metrics.scroll_item,
3780 count,
3781 decoded.data.len(),
3782 virtual_overdraw_items(decoded),
3783 );
3784 }
3785 } else if decoded.reveal_key.is_none() && !decoded.bottom_align && !decoded.follow_tail {
3786 extend_virtual_window(&mut indices, 0, count, decoded.data.len(), 0);
3787 }
3788 drop(guard);
3789
3790 if let Some(reveal) = decoded.reveal_key.as_deref()
3791 && let Some(index) =
3792 (0..decoded.data.len()).find(|index| decoded.data.key(*index) == Some(reveal))
3793 {
3794 extend_virtual_window(
3795 &mut indices,
3796 index,
3797 count,
3798 decoded.data.len(),
3799 virtual_overdraw_items(decoded),
3800 );
3801 if let Some(header) = decoded.sticky_headers.range(..=index).next_back() {
3802 indices.insert(*header);
3803 }
3804 }
3805 if (decoded.bottom_align || decoded.follow_tail) && !decoded.data.is_empty() {
3806 let start = decoded.data.len().saturating_sub(count);
3807 indices.extend(start..decoded.data.len());
3808 }
3809 Ok(indices)
3810}
3811
3812fn same_collection_key_order(
3813 current: &crate::VirtualCollectionData,
3814 next: &crate::VirtualCollectionData,
3815) -> bool {
3816 current.len() == next.len()
3817 && (0..current.len()).all(|index| current.key(index) == next.key(index))
3818}
3819
3820fn virtual_overdraw_items(decoded: &DecodedVirtualCollection) -> usize {
3821 nonnegative_usize((decoded.overdraw_pixels / decoded.estimated_height).ceil())
3822}
3823
3824fn extend_virtual_window(
3825 indices: &mut BTreeSet<usize>,
3826 anchor: usize,
3827 count: usize,
3828 len: usize,
3829 leading: usize,
3830) {
3831 let start = anchor.saturating_sub(leading).min(len);
3832 let end = anchor.saturating_add(count).min(len);
3833 indices.extend(start..end);
3834}
3835
3836struct VirtualCollectionContext {
3837 component: ComponentInstancePath,
3838 context: UiContext,
3839 generation: ScriptGeneration,
3840}
3841
3842fn enter_virtual_collection_scope(
3843 active: &ActiveComponentRenderState,
3844 context: UiContext,
3845) -> Result<(), Box<EvalAltResult>> {
3846 let mut guard = active.try_borrow_mut().map_err(|_| {
3847 Box::new(component_render_error(
3848 "component render stack is already borrowed",
3849 ))
3850 })?;
3851 let render = guard.as_mut().ok_or_else(|| {
3852 Box::new(component_render_error(
3853 "virtual_collection may run only during view render",
3854 ))
3855 })?;
3856 render.stack.push(context.component_path().clone());
3857 render.contexts.push(context);
3858 render.effect_keys.push(None);
3859 Ok(())
3860}
3861
3862fn virtual_collection_context(
3863 active: &ActiveComponentRenderState,
3864) -> Result<VirtualCollectionContext, Box<EvalAltResult>> {
3865 let guard = active.try_borrow().map_err(|_| {
3866 Box::new(component_render_error(
3867 "component render stack is already borrowed",
3868 ))
3869 })?;
3870 let render = guard.as_ref().ok_or_else(|| {
3871 Box::new(component_render_error(
3872 "virtual_collection may run only during view render",
3873 ))
3874 })?;
3875 let context = render.contexts.last().cloned().ok_or_else(|| {
3876 Box::new(component_render_error(
3877 "component render context stack is empty",
3878 ))
3879 })?;
3880 Ok(VirtualCollectionContext {
3881 component: context.component_path().clone(),
3882 context: context.clone(),
3883 generation: render.generation,
3884 })
3885}
3886
3887fn collection_string(config: &mut Map, name: &str) -> Result<String, Box<EvalAltResult>> {
3888 config
3889 .remove(name)
3890 .and_then(Dynamic::try_cast::<ImmutableString>)
3891 .map(|value| value.to_string())
3892 .ok_or_else(|| {
3893 Box::new(component_render_error(format!(
3894 "virtual collection `{name}` must be a string"
3895 )))
3896 })
3897}
3898
3899fn collection_optional_string(
3900 config: &mut Map,
3901 name: &str,
3902) -> Result<Option<String>, Box<EvalAltResult>> {
3903 config
3904 .remove(name)
3905 .map(|value| {
3906 value
3907 .try_cast::<ImmutableString>()
3908 .map(|value| value.to_string())
3909 .ok_or_else(|| {
3910 Box::new(component_render_error(format!(
3911 "virtual collection `{name}` must be a string"
3912 )))
3913 })
3914 })
3915 .transpose()
3916}
3917
3918fn collection_optional_number(
3919 config: &mut Map,
3920 name: &str,
3921) -> Result<Option<f64>, Box<EvalAltResult>> {
3922 config
3923 .remove(name)
3924 .map(|value| {
3925 if value.is::<rhai::FLOAT>() {
3926 Ok(value.cast::<rhai::FLOAT>())
3927 } else if value.is::<rhai::INT>() {
3928 value
3929 .cast::<rhai::INT>()
3930 .to_string()
3931 .parse::<f64>()
3932 .map_err(|_| Box::new(component_render_error("invalid collection number")))
3933 } else {
3934 Err(Box::new(component_render_error(format!(
3935 "virtual collection `{name}` must be a number"
3936 ))))
3937 }
3938 })
3939 .transpose()
3940}
3941
3942fn collection_positive(config: &mut Map, name: &str) -> Result<f64, Box<EvalAltResult>> {
3943 let value = collection_optional_number(config, name)?.ok_or_else(|| {
3944 Box::new(component_render_error(format!(
3945 "virtual collection `{name}` is required"
3946 )))
3947 })?;
3948 if value.is_finite() && value > 0.0 {
3949 Ok(value)
3950 } else {
3951 Err(Box::new(component_render_error(format!(
3952 "virtual collection `{name}` must be finite and positive"
3953 ))))
3954 }
3955}
3956
3957fn collection_optional_bool(
3958 config: &mut Map,
3959 name: &str,
3960) -> Result<Option<bool>, Box<EvalAltResult>> {
3961 config
3962 .remove(name)
3963 .map(|value| {
3964 value.try_cast::<bool>().ok_or_else(|| {
3965 Box::new(component_render_error(format!(
3966 "virtual collection `{name}` must be bool"
3967 )))
3968 })
3969 })
3970 .transpose()
3971}
3972
3973fn collection_optional_indices(
3974 config: &mut Map,
3975 name: &str,
3976) -> Result<Option<BTreeSet<usize>>, Box<EvalAltResult>> {
3977 let Some(value) = config.remove(name) else {
3978 return Ok(None);
3979 };
3980 let values = value.try_cast::<Array>().ok_or_else(|| {
3981 Box::new(component_render_error(format!(
3982 "virtual collection `{name}` must be an array of non-negative integers"
3983 )))
3984 })?;
3985 let mut indices = BTreeSet::new();
3986 for (position, value) in values.into_iter().enumerate() {
3987 let index = value.try_cast::<rhai::INT>().ok_or_else(|| {
3988 Box::new(component_render_error(format!(
3989 "virtual collection `{name}` item {position} must be an integer"
3990 )))
3991 })?;
3992 let index = usize::try_from(index).map_err(|_| {
3993 Box::new(component_render_error(format!(
3994 "virtual collection `{name}` item {position} must be non-negative"
3995 )))
3996 })?;
3997 if !indices.insert(index) {
3998 return Err(Box::new(component_render_error(format!(
3999 "virtual collection `{name}` contains duplicate index {index}"
4000 ))));
4001 }
4002 }
4003 Ok(Some(indices))
4004}
4005
4006fn collection_data(config: &mut Map) -> Result<crate::VirtualCollectionData, Box<EvalAltResult>> {
4007 let data = config.remove("data").ok_or_else(|| {
4008 Box::new(component_render_error(
4009 "virtual collection `data` is required",
4010 ))
4011 })?;
4012 if data.is::<crate::NativeCollection>() {
4013 return Ok(crate::VirtualCollectionData::Native(
4014 data.cast::<crate::NativeCollection>(),
4015 ));
4016 }
4017 let data = data.try_cast::<Array>().ok_or_else(|| {
4018 Box::new(component_render_error(
4019 "virtual collection `data` must be an array or NativeCollection",
4020 ))
4021 })?;
4022 let data = data
4023 .into_iter()
4024 .map(|value| {
4025 UiValue::from_dynamic(value)
4026 .map_err(|error| Box::new(component_render_error(error.to_string())))
4027 })
4028 .collect::<Result<Vec<_>, _>>()?;
4029 let mut keys = BTreeSet::new();
4030 for (index, item) in data.iter().enumerate() {
4031 let (key, _) = collection_payload(item, index)?;
4032 if !keys.insert(key.clone()) {
4033 return Err(Box::new(component_render_error(format!(
4034 "virtual collection data key `{key}` is duplicated"
4035 ))));
4036 }
4037 }
4038 Ok(crate::VirtualCollectionData::Values(data))
4039}
4040
4041fn collection_payload(item: &UiValue, index: usize) -> Result<(String, Map), Box<EvalAltResult>> {
4042 let UiValue::Map(map) = item else {
4043 return Err(Box::new(component_render_error(
4044 "virtual collection data items must be maps with a string `key`",
4045 )));
4046 };
4047 let key = match map.get("key") {
4048 Some(UiValue::String(key)) if !key.is_empty() => key.clone(),
4049 _ => {
4050 return Err(Box::new(component_render_error(
4051 "virtual collection data item `key` must be a non-empty string",
4052 )));
4053 }
4054 };
4055 Ok((
4056 key.clone(),
4057 Map::from_iter([
4058 ("key".into(), Dynamic::from(key)),
4059 (
4060 "index".into(),
4061 Dynamic::from_int(rhai::INT::try_from(index).unwrap_or(rhai::INT::MAX)),
4062 ),
4063 ("item".into(), item.clone().into_dynamic()),
4064 ]),
4065 ))
4066}
4067
4068fn add_collection_neighbors(data: &crate::VirtualCollectionData, index: usize, payload: &mut Map) {
4069 let key = |index: Option<usize>| {
4070 index
4071 .and_then(|index| data.key(index))
4072 .map_or(Dynamic::UNIT, |key| Dynamic::from(key.to_owned()))
4073 };
4074 payload.insert("previous_key".into(), key(index.checked_sub(1)));
4075 payload.insert(
4076 "next_key".into(),
4077 key(index.checked_add(1).filter(|index| *index < data.len())),
4078 );
4079 payload.insert("first_key".into(), key((!data.is_empty()).then_some(0)));
4080 payload.insert("last_key".into(), key(data.len().checked_sub(1)));
4081}
4082
4083fn nonnegative_usize(value: f64) -> usize {
4084 value.to_string().parse().unwrap_or(usize::MAX)
4085}
4086
4087fn component_event_callbacks(
4088 component: &crate::ComponentDefinition,
4089 props: &Map,
4090) -> Vec<(String, FnPtr)> {
4091 component
4092 .schema
4093 .events
4094 .keys()
4095 .filter_map(|event| {
4096 props
4097 .get(format!("on_{event}").as_str())
4098 .filter(|value| value.is::<FnPtr>())
4099 .map(|value| (event.clone(), value.clone_cast::<FnPtr>()))
4100 })
4101 .collect()
4102}
4103
4104fn bind_component_callback_props(
4105 props: &mut Map,
4106 schema: &BTreeMap<String, crate::ObjectField>,
4107 binding: &ComponentCallbackBinding,
4108) {
4109 for (name, field) in schema {
4110 if schema_contains_callback(&field.schema)
4111 && let Some(value) = props.get_mut(name.as_str())
4112 {
4113 bind_component_callback_value(&field.schema, value, binding);
4114 }
4115 }
4116}
4117
4118fn schema_contains_callback(schema: &crate::ValueSchema) -> bool {
4119 match schema {
4120 crate::ValueSchema::Callback => true,
4121 crate::ValueSchema::Array { items, .. }
4122 | crate::ValueSchema::Map { values: items }
4123 | crate::ValueSchema::Optional { value: items } => schema_contains_callback(items),
4124 crate::ValueSchema::Object { fields, .. } => fields
4125 .values()
4126 .any(|field| schema_contains_callback(&field.schema)),
4127 crate::ValueSchema::OneOf { variants } => variants.iter().any(schema_contains_callback),
4128 _ => false,
4129 }
4130}
4131
4132fn bind_component_callback_value(
4133 schema: &crate::ValueSchema,
4134 value: &mut Dynamic,
4135 binding: &ComponentCallbackBinding,
4136) {
4137 match schema {
4138 crate::ValueSchema::Callback if value.is::<FnPtr>() => {
4139 let mut function = value.clone_cast::<FnPtr>();
4140 if !function
4141 .iter_curry()
4142 .any(Dynamic::is::<ComponentCallbackBinding>)
4143 {
4144 function.add_curry(Dynamic::from(binding.clone()));
4145 *value = Dynamic::from(function);
4146 }
4147 }
4148 crate::ValueSchema::Optional { value: inner } if !value.is_unit() => {
4149 bind_component_callback_value(inner, value, binding);
4150 }
4151 crate::ValueSchema::OneOf { variants } => {
4152 if let Some(variant) = variants
4153 .iter()
4154 .find(|variant| variant.validate(value).is_ok())
4155 {
4156 bind_component_callback_value(variant, value, binding);
4157 }
4158 }
4159 crate::ValueSchema::Array { items, .. }
4160 if schema_contains_callback(items) && value.is::<Array>() =>
4161 {
4162 let mut values = value.clone_cast::<Array>();
4163 for value in &mut values {
4164 bind_component_callback_value(items, value, binding);
4165 }
4166 *value = Dynamic::from_array(values);
4167 }
4168 crate::ValueSchema::Map {
4169 values: item_schema,
4170 } if schema_contains_callback(item_schema) && value.is::<Map>() => {
4171 let mut values = value.clone_cast::<Map>();
4172 for value in values.values_mut() {
4173 bind_component_callback_value(item_schema, value, binding);
4174 }
4175 *value = Dynamic::from_map(values);
4176 }
4177 crate::ValueSchema::Object { fields, .. } if value.is::<Map>() => {
4178 let mut values = value.clone_cast::<Map>();
4179 for (name, field) in fields {
4180 if schema_contains_callback(&field.schema)
4181 && let Some(value) = values.get_mut(name.as_str())
4182 {
4183 bind_component_callback_value(&field.schema, value, binding);
4184 }
4185 }
4186 *value = Dynamic::from_map(values);
4187 }
4188 _ => {}
4189 }
4190}
4191
4192fn bind_component_node_props(
4193 props: &mut Map,
4194 schema: &BTreeMap<String, crate::ObjectField>,
4195 binding: &ComponentCallbackBinding,
4196) {
4197 for (name, field) in schema {
4198 if schema_contains_node(&field.schema)
4199 && let Some(value) = props.get_mut(name.as_str())
4200 {
4201 bind_component_node_value(&field.schema, value, binding);
4202 }
4203 }
4204}
4205
4206fn schema_contains_node(schema: &crate::ValueSchema) -> bool {
4207 match schema {
4208 crate::ValueSchema::Node => true,
4209 crate::ValueSchema::Array { items, .. }
4210 | crate::ValueSchema::Map { values: items }
4211 | crate::ValueSchema::Optional { value: items } => schema_contains_node(items),
4212 crate::ValueSchema::Object { fields, .. } => fields
4213 .values()
4214 .any(|field| schema_contains_node(&field.schema)),
4215 crate::ValueSchema::OneOf { variants } => variants.iter().any(schema_contains_node),
4216 _ => false,
4217 }
4218}
4219
4220fn bind_component_node_value(
4221 schema: &crate::ValueSchema,
4222 value: &mut Dynamic,
4223 binding: &ComponentCallbackBinding,
4224) {
4225 match schema {
4226 crate::ValueSchema::Node if value.is::<UiNode>() => {
4227 let mut node = value.clone_cast::<UiNode>();
4228 node.bind_component_scope(
4229 &binding.component,
4230 binding.incarnation,
4231 &binding.events,
4232 binding.context.as_ref(),
4233 );
4234 node.activate_component_snapshots();
4235 *value = Dynamic::from(node);
4236 }
4237 crate::ValueSchema::Optional { value: inner } if !value.is_unit() => {
4238 bind_component_node_value(inner, value, binding);
4239 }
4240 crate::ValueSchema::OneOf { variants } => {
4241 if let Some(variant) = variants
4242 .iter()
4243 .find(|variant| variant.validate(value).is_ok())
4244 {
4245 bind_component_node_value(variant, value, binding);
4246 }
4247 }
4248 crate::ValueSchema::Array { items, .. }
4249 if schema_contains_node(items) && value.is::<Array>() =>
4250 {
4251 let mut values = value.clone_cast::<Array>();
4252 for value in &mut values {
4253 bind_component_node_value(items, value, binding);
4254 }
4255 *value = Dynamic::from_array(values);
4256 }
4257 crate::ValueSchema::Map {
4258 values: item_schema,
4259 } if schema_contains_node(item_schema) && value.is::<Map>() => {
4260 let mut values = value.clone_cast::<Map>();
4261 for value in values.values_mut() {
4262 bind_component_node_value(item_schema, value, binding);
4263 }
4264 *value = Dynamic::from_map(values);
4265 }
4266 crate::ValueSchema::Object { fields, .. } if value.is::<Map>() => {
4267 let mut values = value.clone_cast::<Map>();
4268 for (name, field) in fields {
4269 if schema_contains_node(&field.schema)
4270 && let Some(value) = values.get_mut(name.as_str())
4271 {
4272 bind_component_node_value(&field.schema, value, binding);
4273 }
4274 }
4275 *value = Dynamic::from_map(values);
4276 }
4277 _ => {}
4278 }
4279}
4280
4281fn hydrate_replayed_node_value(value: &mut Dynamic) {
4282 if value.is::<UiNode>() {
4283 let mut node = value.clone_cast::<UiNode>();
4284 node.hydrate_component_subtrees();
4285 *value = Dynamic::from(node);
4286 } else if value.is::<Array>() {
4287 let mut values = value.clone_cast::<Array>();
4288 for value in &mut values {
4289 hydrate_replayed_node_value(value);
4290 }
4291 *value = Dynamic::from_array(values);
4292 } else if value.is::<Map>() {
4293 let mut values = value.clone_cast::<Map>();
4294 for value in values.values_mut() {
4295 hydrate_replayed_node_value(value);
4296 }
4297 *value = Dynamic::from_map(values);
4298 }
4299}
4300
4301fn component_part_styles(props: &Map) -> BTreeMap<String, crate::Style> {
4302 props
4303 .get("part_styles")
4304 .filter(|value| value.is::<Map>())
4305 .map(Dynamic::clone_cast::<Map>)
4306 .unwrap_or_default()
4307 .into_iter()
4308 .filter_map(|(name, value)| {
4309 value
4310 .is::<crate::Style>()
4311 .then(|| (name.to_string(), value.cast::<crate::Style>()))
4312 })
4313 .collect()
4314}
4315
4316fn component_root_style(props: &Map) -> Option<crate::Style> {
4317 props
4318 .get("style")
4319 .filter(|value| value.is::<crate::Style>())
4320 .map(Dynamic::clone_cast::<crate::Style>)
4321}
4322
4323fn register_component_event_callbacks(
4324 active: &ActiveComponentRenderState,
4325 component: &ComponentInstancePath,
4326 caller: &UiContext,
4327 callbacks: Vec<(String, FnPtr)>,
4328 native_context: &crate::invocation::ScriptInvocationContext,
4329) -> Result<(), Box<EvalAltResult>> {
4330 let mut guard = active.try_borrow_mut().map_err(|_| {
4331 Box::new(component_render_error(
4332 "component render stack is already borrowed",
4333 ))
4334 })?;
4335 let active = guard.as_mut().ok_or_else(|| {
4336 Box::new(component_render_error(
4337 "formal component rendering may run only inside view",
4338 ))
4339 })?;
4340 for (event, function) in callbacks {
4341 let mut callback = ScriptCallback::try_from_fn_ptr(function, active.generation)
4342 .map_err(|error| Box::new(component_render_error(error.to_string())))?;
4343 callback.bind_component_scope_if_unset(
4344 caller.component_path(),
4345 caller.component_incarnation(),
4346 caller.event_schemas().clone(),
4347 );
4348 callback.bind_native_context_if_unset(native_context.clone());
4349 active
4350 .event_handlers
4351 .insert((component.clone(), event), callback);
4352 }
4353 Ok(())
4354}
4355
4356fn component_render_error(message: impl Into<String>) -> EvalAltResult {
4357 EvalAltResult::ErrorRuntime(Dynamic::from(message.into()), Position::NONE)
4358}
4359
4360#[cfg(feature = "charts")]
4361fn register_chart_validation_api(engine: &mut Engine) {
4362 engine.register_fn(
4363 "chart_validate",
4364 |spec: Map, data: Dynamic, key_dimension: Dynamic| -> Result<(), Box<EvalAltResult>> {
4365 let spec = UiValue::from_dynamic(Dynamic::from_map(spec))
4366 .map_err(|error| Box::new(component_render_error(error.to_string())))?;
4367 crate::ChartSpec::from_ui_value(&spec)
4368 .map_err(|error| Box::new(component_render_error(error.to_string())))?;
4369 let key_dimension = if key_dimension.is_unit() {
4370 None
4371 } else if key_dimension.is::<ImmutableString>() {
4372 Some(key_dimension.cast::<ImmutableString>().to_string())
4373 } else {
4374 return Err(Box::new(component_render_error(
4375 "chart key_dimension must be a string or null",
4376 )));
4377 };
4378 if data.is::<crate::NativeChartData>() {
4379 return Ok(());
4380 }
4381 if !data.is::<Array>() {
4382 return Err(Box::new(component_render_error(
4383 "chart data must be an array of objects or NativeChartData",
4384 )));
4385 }
4386 let data = UiValue::from_dynamic(Dynamic::from_array(data.cast::<Array>()))
4387 .map_err(|error| Box::new(component_render_error(error.to_string())))?;
4388 let UiValue::Array(rows) = data else {
4389 unreachable!()
4390 };
4391 if rows.len() > 10_000 {
4392 return Err(Box::new(component_render_error(
4393 "inline chart data is limited to 10000 rows; use NativeChartData",
4394 )));
4395 }
4396 let rows = rows
4397 .into_iter()
4398 .enumerate()
4399 .map(|(index, row)| match row {
4400 UiValue::Map(row) => Ok(row),
4401 _ => Err(Box::new(component_render_error(format!(
4402 "chart inline row {index} must be an object"
4403 )))),
4404 })
4405 .collect::<Result<Vec<_>, _>>()?;
4406 crate::ChartDataset::from_rows(
4407 "main",
4408 &rows,
4409 key_dimension,
4410 crate::ChartDataLimits::default(),
4411 )
4412 .map_err(|error| Box::new(component_render_error(error.to_string())))?;
4413 Ok(())
4414 },
4415 );
4416}
4417
4418#[cfg(test)]
4419mod tests {
4420 use super::*;
4421
4422 fn virtual_config(sticky: &[i64], alignment: &str) -> Map {
4423 let mut item = Map::new();
4424 item.insert("key".into(), Dynamic::from("header"));
4425 let mut config = Map::new();
4426 config.insert("key".into(), Dynamic::from("sections"));
4427 config.insert(
4428 "data".into(),
4429 Dynamic::from_array(vec![Dynamic::from_map(item)]),
4430 );
4431 config.insert("estimated_height".into(), Dynamic::from_float(30.0));
4432 config.insert("height".into(), Dynamic::from_float(120.0));
4433 config.insert("alignment".into(), Dynamic::from(alignment.to_owned()));
4434 config.insert(
4435 "sticky_headers".into(),
4436 Dynamic::from_array(sticky.iter().copied().map(Dynamic::from_int).collect()),
4437 );
4438 config
4439 }
4440
4441 #[test]
4442 fn sticky_virtual_collection_indices_are_bounded_unique_and_top_aligned() {
4443 let decoded = decode_virtual_collection(virtual_config(&[0], "top")).unwrap();
4444 assert_eq!(decoded.sticky_headers.as_ref(), &BTreeSet::from([0]));
4445
4446 for (sticky, alignment, expected) in [
4447 (&[1][..], "top", "outside 1 items"),
4448 (&[0, 0][..], "top", "duplicate index 0"),
4449 (&[0][..], "bottom", "require top-aligned"),
4450 ] {
4451 let error = decode_virtual_collection(virtual_config(sticky, alignment)).unwrap_err();
4452 assert!(error.to_string().contains(expected), "{error}");
4453 }
4454 }
4455
4456 #[test]
4457 fn virtual_collection_reveal_key_is_known_and_has_one_scroll_owner() {
4458 let mut config = virtual_config(&[], "top");
4459 config.insert("reveal_key".into(), Dynamic::from("header"));
4460 let decoded = decode_virtual_collection(config).unwrap();
4461 assert_eq!(decoded.reveal_key.as_deref(), Some("header"));
4462
4463 let mut unknown = virtual_config(&[], "top");
4464 unknown.insert("reveal_key".into(), Dynamic::from("missing"));
4465 let error = decode_virtual_collection(unknown).unwrap_err();
4466 assert!(error.to_string().contains("is not present"), "{error}");
4467
4468 let mut competing = virtual_config(&[], "top");
4469 competing.insert("reveal_key".into(), Dynamic::from("header"));
4470 competing.insert("follow_tail".into(), Dynamic::from(true));
4471 let error = decode_virtual_collection(competing).unwrap_err();
4472 assert!(error.to_string().contains("must not combine"), "{error}");
4473 }
4474
4475 #[test]
4476 fn script_builds_a_declarative_tree() {
4477 let mut runtime = RuntimeEngine::new();
4478 let compiled = runtime
4479 .compile(
4480 r#"
4481 fn view() {
4482 column([
4483 text("Hello").with_key("greeting"),
4484 text("from Rhai")
4485 ])
4486 }
4487 "#,
4488 )
4489 .expect("script should compile");
4490
4491 let root = runtime.render(&compiled).expect("view should render");
4492 assert_eq!(
4493 root.source().map(|source| source.module.as_str()),
4494 Some("ui/main.rhai")
4495 );
4496 let crate::UiNodeKind::Box { children } = root.kind() else {
4497 panic!("column must render a box");
4498 };
4499 assert_eq!(children.len(), 2);
4500 assert_eq!(
4501 children[0].key().map(crate::NodeKey::as_str),
4502 Some("greeting")
4503 );
4504 assert!(children.iter().all(|child| child.source().is_some()));
4505 }
4506
4507 #[test]
4508 fn runtime_emits_official_rhai_language_server_definitions() {
4509 let runtime = RuntimeEngine::new();
4510 let definitions = runtime.definition_source();
4511 assert!(definitions.starts_with("module static;"));
4512 assert!(definitions.contains("fn box"));
4513 assert!(definitions.contains("fn timeout"));
4514 assert!(definitions.contains("fn canvas_fill_path"));
4515 assert!(definitions.contains("UiNode"));
4516 }
4517
4518 #[test]
4519 fn retained_callbacks_reject_capturing_functions_and_non_data_curry() {
4520 let mut runtime = RuntimeEngine::new();
4521 let compiled = runtime
4522 .compile(r#"fn view() { text("x").on_click(|| ()) }"#)
4523 .unwrap();
4524 let error = runtime.render(&compiled).unwrap_err();
4525 assert!(
4526 error
4527 .to_string()
4528 .contains("anonymous or capturing functions")
4529 );
4530
4531 let mut callback = FnPtr::new("clicked").unwrap();
4532 callback.add_curry(Dynamic::from(UiNode::text("not durable data")));
4533 assert!(matches!(
4534 ScriptCallback::try_from_fn_ptr(callback, ScriptGeneration::initial()),
4535 Err(ScriptCallbackDefinitionError::InvalidCurry { index: 0, .. })
4536 ));
4537 }
4538
4539 #[test]
4540 fn replay_hydrates_formal_subtrees_inside_nested_dynamic_node_shapes() {
4541 let root = ComponentInstancePath::root("View", "main");
4542 let outer = root.child("Outer", "outer");
4543 let inner = root.child("Inner", "inner");
4544 let inner_snapshot = crate::node::ComponentOwnedSnapshot::default();
4545 let mut current_inner = UiNode::text("current")
4546 .with_component_root_snapshot(inner.clone(), inner_snapshot.reference());
4547 current_inner.activate_component_snapshots();
4548 let outer_snapshot = crate::node::ComponentOwnedSnapshot::default();
4549 let mut current_outer = UiNode::box_node(vec![
4550 UiNode::text("outer"),
4551 UiNode::text("stale-inner")
4552 .with_component_root_snapshot(inner, inner_snapshot.reference()),
4553 ])
4554 .with_component_root_snapshot(outer.clone(), outer_snapshot.reference());
4555 current_outer.activate_component_snapshots();
4556 let stale = UiNode::box_node(vec![
4557 UiNode::text("stale-outer")
4558 .with_component_root_snapshot(outer, outer_snapshot.reference())
4559 .with_attribute("presentation", UiValue::Bool(true)),
4560 ]);
4561 let mut nested = Map::new();
4562 nested.insert(
4563 "object".into(),
4564 Dynamic::from_map(Map::from_iter([(
4565 "nodes".into(),
4566 Dynamic::from_array(vec![Dynamic::from(stale)]),
4567 )])),
4568 );
4569 let mut value = Dynamic::from_map(nested);
4570
4571 hydrate_replayed_node_value(&mut value);
4572
4573 let nested = value.cast::<Map>();
4574 let object = nested["object"].clone_cast::<Map>();
4575 let nodes = object["nodes"].clone_cast::<Array>();
4576 let node = nodes[0].clone_cast::<UiNode>();
4577 let crate::UiNodeKind::Box { children } = node.kind() else {
4578 panic!("nested node shape must preserve its raw wrapper");
4579 };
4580 assert_eq!(
4581 children[0].attributes().get("presentation"),
4582 Some(&UiValue::Bool(true))
4583 );
4584 let crate::UiNodeKind::Box { children } = children[0].kind() else {
4585 panic!("outer component must hydrate its owned snapshot");
4586 };
4587 assert!(
4588 matches!(children[1].kind(), crate::UiNodeKind::Text { text } if text == "current")
4589 );
4590 }
4591
4592 #[test]
4593 fn final_component_api_registers_render_once_and_invokes_by_id() {
4594 let mut runtime = RuntimeEngine::new();
4595 let compiled = runtime
4596 .compile(
4597 r#"
4598 define_component(#{
4599 metadata: #{
4600 id: "components/message", "export": "Message", version: "0.1.0",
4601 runtime_api: #{ min_inclusive: 2, max_exclusive: 3 },
4602 dependencies: [], capabilities: #{}
4603 },
4604 schema: #{ props: #{}, state: #{ fields: #{} }, events: #{}, slots: #{}, parts: [] },
4605 render: Fn("render_Message")
4606 });
4607 fn Message(props) { render_component("components/message", props) }
4608 fn render_Message(ctx, props) { text("final component api") }
4609 fn view() { Message(#{ key: "message" }) }
4610 "#,
4611 )
4612 .unwrap();
4613 let root = runtime.render(&compiled).unwrap();
4614 assert!(matches!(
4615 root.kind(),
4616 crate::UiNodeKind::Text { text } if text == "final component api"
4617 ));
4618 let recipe = runtime.component_invocations().next().unwrap();
4619 assert_eq!(recipe.render_name(), "render_Message");
4620 }
4621
4622 #[test]
4623 fn self_contained_compile_rejects_dynamic_imports_before_evaluation() {
4624 let mut runtime = RuntimeEngine::new();
4625 assert!(matches!(
4626 runtime.compile_self_contained_named(
4627 "ui/dynamic_import.rhai",
4628 "let path = \"components/button\"; import path as button;",
4629 ),
4630 Err(RuntimeError::Import(_))
4631 ));
4632 }
4633
4634 #[test]
4635 fn script_builds_an_error_boundary() {
4636 let mut runtime = RuntimeEngine::new();
4637 let compiled = runtime
4638 .compile(
4639 r#"
4640 fn view() {
4641 error_boundary(text("content"), text("fallback"))
4642 }
4643 "#,
4644 )
4645 .unwrap();
4646 let root = runtime.render(&compiled).unwrap();
4647 assert!(matches!(
4648 root.kind(),
4649 crate::UiNodeKind::ErrorBoundary { .. }
4650 ));
4651 }
4652
4653 #[test]
4654 fn lazy_error_boundary_catches_rhai_evaluation_failure() {
4655 let mut runtime = RuntimeEngine::new();
4656 let compiled = runtime
4657 .compile(
4658 r#"
4659 fn broken() { throw "boom"; }
4660 fn fallback() { text("fallback") }
4661 fn view() { error_boundary_lazy(Fn("broken"), Fn("fallback")) }
4662 "#,
4663 )
4664 .unwrap();
4665 let root = runtime.render(&compiled).unwrap();
4666 assert!(matches!(root.kind(), crate::UiNodeKind::Text { text } if text == "fallback"));
4667 assert!(matches!(
4668 root.attributes().get("boundary_error"),
4669 Some(crate::UiValue::String(message)) if message.contains("boom")
4670 ));
4671 }
4672
4673 #[test]
4674 fn script_builds_a_typed_controlled_overlay() {
4675 let mut runtime = RuntimeEngine::new();
4676 let compiled = runtime
4677 .compile(
4678 r#"
4679 fn changed(ctx, open) { () }
4680 fn view() {
4681 overlay(
4682 text("trigger"),
4683 text("content"),
4684 #{
4685 id: "account",
4686 kind: "popover",
4687 placement: "bottom",
4688 open: true,
4689 gap: 6
4690 }
4691 ).on_open_change(Fn("changed"))
4692 }
4693 "#,
4694 )
4695 .unwrap();
4696 let root = runtime.render(&compiled).unwrap();
4697 assert!(matches!(
4698 root.kind(),
4699 crate::UiNodeKind::Overlay { spec, .. }
4700 if spec.id.as_str() == "account"
4701 && spec.open
4702 && (spec.gap - 6.0).abs() < f64::EPSILON
4703 && spec.placement == crate::OverlayPlacement::Bottom
4704 ));
4705 assert_eq!(
4706 root.handler("open_change")
4707 .unwrap()
4708 .as_script()
4709 .unwrap()
4710 .generation(),
4711 compiled.generation()
4712 );
4713 }
4714
4715 #[test]
4716 fn click_handlers_can_carry_typed_semantic_payloads() {
4717 let mut runtime = RuntimeEngine::new();
4718 let compiled = runtime
4719 .compile(
4720 r#"
4721 fn changed(ctx, value) { () }
4722 fn view() {
4723 text("toggle").on_click_value(
4724 Fn("changed"), #{ checked: true, source: "pointer" }
4725 )
4726 }
4727 "#,
4728 )
4729 .unwrap();
4730 let root = runtime.render(&compiled).unwrap();
4731 assert_eq!(
4732 root.handler_payload("click"),
4733 Some(&crate::UiValue::Map(BTreeMap::from([
4734 ("checked".to_owned(), crate::UiValue::Bool(true)),
4735 (
4736 "source".to_owned(),
4737 crate::UiValue::String("pointer".to_owned()),
4738 ),
4739 ])))
4740 );
4741 assert!(root.handlers().contains_key("click"));
4742 }
4743
4744 #[test]
4745 fn rhai_nodes_retain_ordered_handlers_for_all_event_phases() {
4746 let mut runtime = RuntimeEngine::new();
4747 let compiled = runtime
4748 .compile(
4749 r#"
4750 fn first(ctx, event) { event_response().prevent_default() }
4751 fn second(ctx, event) { event_response().stop_immediate() }
4752 fn capture(ctx, event) { propagate() }
4753 fn view() {
4754 text("drag")
4755 .on_capture("pointer_down", Fn("capture"))
4756 .on("pointer_down", Fn("first"))
4757 .on("pointer_down", Fn("second"))
4758 }
4759 "#,
4760 )
4761 .unwrap();
4762 let root = runtime.render(&compiled).unwrap();
4763 let bindings = root.event_handlers("pointer_down");
4764 assert_eq!(bindings.len(), 3);
4765 assert_eq!(bindings[0].phase(), crate::EventPhase::Capture);
4766 assert_eq!(bindings[1].phase(), crate::EventPhase::Target);
4767 assert_eq!(bindings[2].phase(), crate::EventPhase::Target);
4768 assert_eq!(bindings[2].handler().as_script().unwrap().name(), "second");
4769 }
4770
4771 #[test]
4772 fn rhai_nodes_attach_only_registered_schema_checked_native_handlers() {
4773 let mut runtime = RuntimeEngine::new();
4774 let id = crate::NativeHandlerId::parse("timeline.drag").unwrap();
4775 runtime
4776 .register_native_handler(
4777 crate::NativeHandlerDescriptor::new(
4778 id,
4779 BTreeMap::from([("pointer_down".to_owned(), crate::ValueSchema::UiValue)]),
4780 )
4781 .unwrap(),
4782 |_, _, _, _| Ok(crate::EventResponse::new().capture_pointer()),
4783 )
4784 .unwrap();
4785 let compiled = runtime
4786 .compile(
4787 r#"
4788 fn view() {
4789 text("drag").on(
4790 "pointer_down", native_handler("timeline.drag")
4791 )
4792 }
4793 "#,
4794 )
4795 .unwrap();
4796 let root = runtime.render(&compiled).unwrap();
4797 assert!(matches!(
4798 root.handler("pointer_down"),
4799 Some(crate::UiEventHandler::Native(reference))
4800 if reference.descriptor().id.as_str() == "timeline.drag"
4801 ));
4802
4803 let input = runtime
4804 .compile(
4805 r#"
4806 fn view() {
4807 gpui_rhai::TextInputPrimitive(#{
4808 key: "native-input", value: "", placeholder: "Edit",
4809 disabled: false, read_only: false, typography: "body",
4810 on_change: native_handler("timeline.drag")
4811 })
4812 }
4813 "#,
4814 )
4815 .unwrap();
4816 assert!(
4817 runtime.render(&input).is_ok(),
4818 "native handlers must cross formal component and primitive callback props"
4819 );
4820
4821 let missing = runtime
4822 .compile(
4823 "fn view() { text(\"x\").on(\"pointer_down\", native_handler(\"app.missing\")) }",
4824 )
4825 .unwrap();
4826 assert!(runtime.render(&missing).is_err());
4827 }
4828
4829 #[test]
4830 fn final_box_and_fragment_atoms_are_distinct_snapshots() {
4831 let mut runtime = RuntimeEngine::new();
4832 let compiled = runtime
4833 .compile(
4834 r#"
4835 fn view() {
4836 box([text("a"), fragment([text("b"), text("c")])])
4837 }
4838 "#,
4839 )
4840 .unwrap();
4841 let root = runtime.render(&compiled).unwrap();
4842 let crate::UiNodeKind::Box { children } = root.kind() else {
4843 panic!("root must be the final Box atom");
4844 };
4845 assert!(matches!(
4846 children[1].kind(),
4847 crate::UiNodeKind::Fragment { children } if children.len() == 2
4848 ));
4849 }
4850
4851 #[test]
4852 fn text_accepts_typed_inline_span_runs() {
4853 let mut runtime = RuntimeEngine::new();
4854 let compiled = runtime
4855 .compile(
4856 r#"
4857 fn view() {
4858 text([
4859 span("Hello ").bold(),
4860 span("world").color(rgb(0x22cc88)).italic()
4861 ])
4862 }
4863 "#,
4864 )
4865 .unwrap();
4866 let root = runtime.render(&compiled).unwrap();
4867 let crate::UiNodeKind::RichText { text, spans } = root.kind() else {
4868 panic!("typed spans must produce rich text");
4869 };
4870 assert_eq!(text.as_str(), "Hello world");
4871 assert_eq!(spans.len(), 2);
4872 assert!(spans[0].is_bold());
4873 assert!(spans[1].is_italic());
4874 }
4875
4876 #[test]
4877 fn canvas_scene_is_keyed_validated_and_script_constructible() {
4878 let mut runtime = RuntimeEngine::new();
4879 let compiled = runtime
4880 .compile(
4881 r#"
4882 fn view() {
4883 canvas(canvas_scene([
4884 canvas_rect("panel", 0.0, 0.0, 100.0, 40.0, rgb(0x112233)),
4885 canvas_circle("dot", 20.0, 20.0, 8.0, rgb(0xffcc00)),
4886 canvas_line("axis", 0.0, 39.0, 100.0, 39.0, 1.0, rgb(0xffffff))
4887 ]))
4888 }
4889 "#,
4890 )
4891 .unwrap();
4892 let root = runtime.render(&compiled).unwrap();
4893 assert!(matches!(
4894 root.kind(),
4895 crate::UiNodeKind::Canvas { scene } if scene.commands().len() == 3
4896 ));
4897 }
4898
4899 #[test]
4900 fn inline_svg_atom_is_bounded_and_self_contained() {
4901 let mut runtime = RuntimeEngine::new();
4902 let compiled = runtime
4903 .compile(
4904 r#"
4905 fn view() {
4906 svg("<svg viewBox='0 0 10 10'><path fill='currentColor' d='M0 0L10 10'/></svg>")
4907 }
4908 "#,
4909 )
4910 .unwrap();
4911 let root = runtime.render(&compiled).unwrap();
4912 assert!(matches!(
4913 root.kind(),
4914 crate::UiNodeKind::Svg { source } if source.as_str().contains("currentColor")
4915 ));
4916
4917 let rejected = runtime
4918 .compile(r#"fn view() { svg("<svg><image href='https://example.com/a.png'/></svg>") }"#)
4919 .unwrap();
4920 assert!(runtime.render(&rejected).is_err());
4921 }
4922
4923 #[test]
4924 fn recompilation_invalidates_previous_generation() {
4925 let mut runtime = RuntimeEngine::new();
4926 let first = runtime.compile("fn view() { text(\"first\") }").unwrap();
4927 runtime.render(&first).unwrap();
4928 let second = runtime.compile("fn view() { text(\"second\") }").unwrap();
4929
4930 assert!(runtime.is_current(first.generation()));
4931 assert!(!runtime.is_current(second.generation()));
4932 runtime.render(&second).unwrap();
4933 assert!(!runtime.is_current(first.generation()));
4934 assert!(runtime.is_current(second.generation()));
4935 }
4936
4937 #[test]
4938 fn callbacks_are_rejected_after_reload() {
4939 let mut runtime = RuntimeEngine::new();
4940 let first = runtime
4941 .compile(
4942 r#"
4943 fn view() { text("first") }
4944 fn add_suffix(value) { value + "!" }
4945 "#,
4946 )
4947 .unwrap();
4948 runtime.render(&first).unwrap();
4949 let callback = runtime.callback(&first, "add_suffix").unwrap();
4950
4951 let value = runtime
4952 .invoke_callback(&first, &callback, ("hello",))
4953 .unwrap();
4954 assert_eq!(value.into_immutable_string().unwrap(), "hello!");
4955
4956 let second = runtime.compile("fn view() { text(\"second\") }").unwrap();
4957 runtime.render(&second).unwrap();
4958 assert!(matches!(
4959 runtime.invoke_callback(&second, &callback, ("hello",)),
4960 Err(RuntimeError::StaleCallback { .. })
4961 ));
4962 }
4963
4964 #[test]
4965 fn failed_candidate_keeps_last_good_generation_active() {
4966 let mut runtime = RuntimeEngine::new();
4967 let active = runtime
4968 .compile(
4969 r#"
4970 fn view() { text("active") }
4971 fn action(value) { value }
4972 "#,
4973 )
4974 .unwrap();
4975 runtime.render(&active).unwrap();
4976 let callback = runtime.callback(&active, "action").unwrap();
4977
4978 let candidate = runtime
4979 .compile("fn view() { throw \"render failed\"; }")
4980 .unwrap();
4981 assert!(runtime.render(&candidate).is_err());
4982 assert!(runtime.is_current(active.generation()));
4983 let _ = runtime
4984 .invoke_callback(&active, &callback, ("still active",))
4985 .unwrap();
4986 }
4987
4988 #[test]
4989 fn failed_nested_component_records_the_deepest_component_path() {
4990 let mut runtime = RuntimeEngine::new();
4991 let compiled = runtime
4992 .compile(
4993 r#"
4994 define_component(#{
4995 metadata: #{ id: "components/broken", "export": "Broken",
4996 version: "0.1.3",
4997 runtime_api: #{ min_inclusive: 2, max_exclusive: 3 },
4998 dependencies: [], capabilities: #{} },
4999 schema: #{ props: #{}, state: #{ fields: #{} }, events: #{},
5000 slots: #{}, parts: [] },
5001 render: Fn("render_Broken")
5002 });
5003 fn Broken(props) { render_component("components/broken", props) }
5004 fn render_Broken(ctx, props) { throw "nested failure"; }
5005 fn view() { Broken(#{ key: "inner" }) }
5006 "#,
5007 )
5008 .unwrap();
5009
5010 assert!(runtime.render(&compiled).is_err());
5011 assert_eq!(
5012 runtime
5013 .last_failed_component()
5014 .as_ref()
5015 .map(ToString::to_string),
5016 Some("/App[root]/Broken[inner]".to_owned())
5017 );
5018 assert!(runtime.last_failed_timing().is_some());
5019
5020 let recovered = runtime
5021 .compile("fn view() { text(\"recovered\") }")
5022 .unwrap();
5023 runtime.render(&recovered).unwrap();
5024 assert_eq!(runtime.last_failed_component(), None);
5025 assert_eq!(runtime.last_failed_timing(), None);
5026 }
5027
5028 #[test]
5029 fn execution_timings_cover_compile_and_render() {
5030 let mut runtime = RuntimeEngine::new();
5031 runtime.set_slow_threshold(Duration::ZERO);
5032 let compiled = runtime
5033 .compile(
5034 r#"
5035 fn view() { text("timed") }
5036 fn counted() { let total = 0; for value in 0..100 { total += value; } total }
5037 "#,
5038 )
5039 .unwrap();
5040 runtime.render(&compiled).unwrap();
5041 let callback = runtime.callback(&compiled, "counted").unwrap();
5042 let _ = runtime.invoke_callback(&compiled, &callback, ()).unwrap();
5043 let timings = runtime.take_timings();
5044 assert_eq!(timings.len(), 3);
5045 assert_eq!(timings[0].operation, ExecutionOperation::Compile);
5046 assert_eq!(timings[1].operation, ExecutionOperation::Render);
5047 assert_eq!(
5048 timings[2].operation,
5049 ExecutionOperation::Callback("counted".to_owned())
5050 );
5051 assert_eq!(timings[0].operations, 0);
5052 assert!(timings[1].operations > 0);
5053 assert!(timings[2].operations > timings[1].operations);
5054 assert!(timings.iter().all(|timing| timing.slow && timing.succeeded));
5055 }
5056
5057 #[test]
5058 fn independent_candidates_never_share_callback_authority() {
5059 let mut runtime = RuntimeEngine::new();
5060 let first = runtime
5061 .compile("fn view() { text(\"first\") } fn action() { 1 }")
5062 .unwrap();
5063 let callback = runtime.callback(&first, "action").unwrap();
5064 let second = runtime
5065 .compile("fn view() { text(\"second\") } fn action() { 2 }")
5066 .unwrap();
5067 assert_ne!(first.generation(), second.generation());
5068 runtime.render(&second).unwrap();
5069 assert!(matches!(
5070 runtime.invoke_callback(&second, &callback, ()),
5071 Err(RuntimeError::StaleCallback { .. })
5072 ));
5073 }
5074
5075 #[test]
5076 fn callbacks_cannot_cross_runtime_engines() {
5077 let mut first_engine = RuntimeEngine::new();
5078 let first = first_engine
5079 .compile("fn view() { text(\"first\") } fn action() { 1 }")
5080 .unwrap();
5081 first_engine.render(&first).unwrap();
5082 let callback = first_engine.callback(&first, "action").unwrap();
5083
5084 let mut second_engine = RuntimeEngine::new();
5085 let second = second_engine
5086 .compile("fn view() { text(\"second\") } fn action() { 2 }")
5087 .unwrap();
5088 second_engine.render(&second).unwrap();
5089 assert!(matches!(
5090 second_engine.invoke_callback(&second, &callback, ()),
5091 Err(RuntimeError::StaleCallback { .. })
5092 ));
5093 }
5094
5095 #[test]
5096 fn one_program_preparation_shares_identity_across_module_and_entry_compilation() {
5097 let mut runtime = RuntimeEngine::new();
5098 let (module, entry) = runtime
5099 .with_program_preparation(|runtime| {
5100 Ok::<_, RuntimeError>((
5101 runtime.compile_named("module", "fn helper() { 1 }")?,
5102 runtime.compile_named("entry", "fn view() { text(\"entry\") }")?,
5103 ))
5104 })
5105 .unwrap();
5106 assert_eq!(module.generation(), entry.generation());
5107 let next = runtime.compile("fn view() { text(\"next\") }").unwrap();
5108 assert_ne!(entry.generation(), next.generation());
5109 }
5110
5111 #[test]
5112 fn unsafe_const_container_assignment_is_rejected_before_evaluation() {
5113 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
5114 RuntimeEngine::new()
5115 .compile("fn view() { const m = #{ x: 1 }; m.x = 2; text(\"done\") }")
5116 }));
5117 assert!(matches!(
5118 result,
5119 Ok(Err(RuntimeError::InvalidAssignmentTarget(_)))
5120 ));
5121 }
5122
5123 #[test]
5124 fn default_runtime_rejects_filesystem_imports() {
5125 let directory = tempfile::tempdir().unwrap();
5126 let module = directory.path().join("external.rhai");
5127 std::fs::write(&module, "fn value() { \"external\" }").unwrap();
5128 let mut runtime = RuntimeEngine::new();
5129 let source = format!(
5130 "import {:?} as external; fn view() {{ text(external::value()) }}",
5131 module.with_extension("").to_string_lossy()
5132 );
5133 let compiled = runtime.compile(&source).unwrap();
5134 assert!(runtime.render(&compiled).is_err());
5135 }
5136
5137 #[test]
5138 fn nested_component_work_consumes_one_shared_operation_budget() {
5139 let module = r#"
5140 define_component(#{
5141 metadata: #{ id: "components/heavy", "export": "Heavy", version: "0.1.1",
5142 runtime_api: #{ min_inclusive: 2, max_exclusive: 3 }, dependencies: [], capabilities: #{} },
5143 schema: #{ props: #{}, state: #{ fields: #{} }, events: #{}, slots: #{}, parts: ["root"] },
5144 render: Fn("render_Heavy")
5145 });
5146 fn Heavy(props) { render_component("components/heavy", props) }
5147 fn render_Heavy(ctx, props) {
5148 let n = 0;
5149 for i in 0..200000 { n += 1; }
5150 text(`${n}`)
5151 }
5152 "#;
5153 let mut resolver = crate::RestrictedModuleResolver::new();
5154 resolver.insert("components/heavy", module).unwrap();
5155 let mut runtime = RuntimeEngine::new();
5156 runtime.set_module_resolver(resolver);
5157 let compiled = runtime
5158 .compile_self_contained_named(
5159 "budget",
5160 r#"import "components/heavy" as h;
5161 fn view() { column([h::Heavy(#{}), h::Heavy(#{}), h::Heavy(#{}), h::Heavy(#{} )]) }"#,
5162 )
5163 .unwrap();
5164 let result = runtime.render(&compiled);
5165 assert!(result.is_err(), "nested work must exceed the shared budget");
5166 assert!(
5167 runtime
5168 .take_timings()
5169 .iter()
5170 .any(|timing| timing.operations > DEFAULT_SCRIPT_OPERATION_LIMIT)
5171 );
5172 }
5173
5174 #[test]
5175 fn operation_limit_is_host_configurable() {
5176 let heavy = r"fn view() { let n = 0; for i in 0..400000 { n += 1; } text(`${n}`) }";
5177 let mut runtime = RuntimeEngine::new();
5178 assert_eq!(runtime.operation_limit(), DEFAULT_SCRIPT_OPERATION_LIMIT);
5179 let compiled = runtime
5180 .compile_self_contained_named("limit", heavy)
5181 .unwrap();
5182 assert!(
5183 runtime.render(&compiled).is_err(),
5184 "this finite workload actually exceeds the default budget"
5185 );
5186 assert!(runtime.last_failed_timing().unwrap().operations > DEFAULT_SCRIPT_OPERATION_LIMIT);
5187
5188 runtime.set_operation_limit(10_000);
5189 let compiled = runtime
5190 .compile_self_contained_named("limit-low", heavy)
5191 .unwrap();
5192 assert!(
5193 runtime.render(&compiled).is_err(),
5194 "a lowered budget rejects the same script"
5195 );
5196
5197 runtime.set_operation_limit(50_000_000);
5198 let compiled = runtime
5199 .compile_self_contained_named("limit-high", heavy)
5200 .unwrap();
5201 assert!(
5202 runtime.render(&compiled).is_ok(),
5203 "a raised budget admits heavier one-shot work"
5204 );
5205 }
5206
5207 #[test]
5208 fn host_policies_are_independent_and_reload_candidates_do_not_share_mutable_quota() {
5209 let mut engine = RuntimeEngine::new();
5210 assert_eq!(engine.expression_depth_limits(), (64, 32));
5211 engine.set_operation_limit(2_000_000);
5212 engine.set_expression_depth_limits(80, 64);
5213 assert_eq!(engine.engine().max_call_levels(), 64);
5214 assert_eq!(engine.engine().max_array_size(), RHAI_MAX_ARRAY_SIZE);
5215 #[cfg(feature = "dev-reload")]
5216 {
5217 let mut candidate = engine.candidate_engine();
5218 assert_eq!(candidate.operation_limit(), 2_000_000);
5219 assert_eq!(candidate.expression_depth_limits(), (80, 64));
5220 candidate.set_operation_limit(10);
5221 candidate.set_expression_depth_limits(20, 10);
5222 assert_eq!(engine.operation_limit(), 2_000_000);
5223 assert_eq!(engine.expression_depth_limits(), (80, 64));
5224 }
5225 engine.set_operation_limit(0);
5226 engine.set_expression_depth_limits(0, 0);
5227 assert_eq!(engine.operation_limit(), 1);
5228 assert_eq!(engine.expression_depth_limits(), (1, 1));
5229 }
5230
5231 #[test]
5232 fn retained_component_budget_ignores_parent_history_without_resetting_session_total() {
5233 let module = r#"
5234 define_component(#{
5235 metadata: #{ id: "components/probe", "export": "Probe", version: "0.1.1",
5236 runtime_api: #{ min_inclusive: 2, max_exclusive: 3 }, dependencies: [], capabilities: #{} },
5237 schema: #{ props: #{ key: #{ schema: #{ type: "string" }, required: true, sensitive: false } },
5238 state: #{ fields: #{ heavy: #{ schema: #{ type: "bool" }, "default": #{ type: "bool", value: false } } } },
5239 events: #{}, slots: #{}, parts: ["root"] },
5240 render: Fn("render_Probe")
5241 });
5242 fn Probe(props) { render_component("components/probe", props) }
5243 fn render_Probe(ctx, props) {
5244 let n = 0;
5245 if ctx.get_state("heavy") { for i in 0..100000 { n += 1; } }
5246 text(`${n}`)
5247 }
5248 "#;
5249 let mut measured = Vec::new();
5250 for parent_work in [0, 250_000] {
5251 let mut resolver = crate::RestrictedModuleResolver::new();
5252 resolver.insert("components/probe", module).unwrap();
5253 let mut engine = RuntimeEngine::new();
5254 engine.set_module_resolver(resolver);
5255 let source = format!(
5256 r#"import "components/probe" as probe;
5257 fn view(ctx) {{
5258 let n = 0;
5259 for i in 0..{parent_work} {{ n += 1; }}
5260 probe::Probe(#{{ key: "probe" }})
5261 }}"#
5262 );
5263 let compiled = engine
5264 .compile_self_contained_named("incremental-budget", &source)
5265 .unwrap();
5266 let runtime = Rc::new(RefCell::new(crate::UiRuntimeState::new()));
5267 let mut lifecycle = crate::ScriptLifecycle::new(
5268 compiled,
5269 Rc::clone(&runtime),
5270 ComponentInstancePath::root("App", "budget"),
5271 None,
5272 BTreeMap::new(),
5273 &ComponentStateSchema::default(),
5274 )
5275 .unwrap();
5276 lifecycle.start(&mut engine).unwrap();
5277 let owner = engine
5278 .component_invocations()
5279 .next()
5280 .unwrap()
5281 .path()
5282 .clone();
5283 let _ = engine.take_timings();
5284 runtime
5285 .borrow_mut()
5286 .set_component_state_from_host(&owner, "heavy", UiValue::Bool(true))
5287 .unwrap();
5288 assert!(lifecycle.render_dirty(&mut engine).unwrap());
5289 let operations = engine
5290 .take_timings()
5291 .into_iter()
5292 .filter(|timing| timing.operation == ExecutionOperation::Render)
5293 .map(|timing| timing.operations)
5294 .sum::<u64>();
5295 assert!(
5296 operations < DEFAULT_SCRIPT_OPERATION_LIMIT / 2,
5297 "{operations}"
5298 );
5299 measured.push(operations);
5300 }
5301 assert_eq!(measured[0], measured[1]);
5302 }
5303
5304 #[test]
5305 fn multiple_dirty_components_share_one_incremental_budget() {
5306 let module = r#"
5307 define_component(#{
5308 metadata: #{ id: "components/probe", "export": "Probe", version: "0.1.1",
5309 runtime_api: #{ min_inclusive: 2, max_exclusive: 3 }, dependencies: [], capabilities: #{} },
5310 schema: #{ props: #{ key: #{ schema: #{ type: "string" }, required: true, sensitive: false } },
5311 state: #{ fields: #{ heavy: #{ schema: #{ type: "bool" }, "default": #{ type: "bool", value: false } } } },
5312 events: #{}, slots: #{}, parts: ["root"] }, render: Fn("render_Probe") }
5313 );
5314 fn Probe(props) { render_component("components/probe", props) }
5315 fn render_Probe(ctx, props) {
5316 let n = 0;
5317 if ctx.get_state("heavy") { for i in 0..200000 { n += 1; } }
5318 text(`${n}`)
5319 }
5320 "#;
5321 let mut resolver = crate::RestrictedModuleResolver::new();
5322 resolver.insert("components/probe", module).unwrap();
5323 let mut engine = RuntimeEngine::new();
5324 engine.set_module_resolver(resolver);
5325 let compiled = engine
5326 .compile_self_contained_named(
5327 "incremental-aggregate-budget",
5328 r#"import "components/probe" as probe;
5329 fn view(ctx) {
5330 column([
5331 probe::Probe(#{ key: "first" }),
5332 probe::Probe(#{ key: "second" }),
5333 ])
5334 }"#,
5335 )
5336 .unwrap();
5337 let runtime = Rc::new(RefCell::new(crate::UiRuntimeState::new()));
5338 let mut lifecycle = crate::ScriptLifecycle::new(
5339 compiled,
5340 Rc::clone(&runtime),
5341 ComponentInstancePath::root("App", "budget"),
5342 None,
5343 BTreeMap::new(),
5344 &ComponentStateSchema::default(),
5345 )
5346 .unwrap();
5347 lifecycle.start(&mut engine).unwrap();
5348 let owners = engine
5349 .component_invocations()
5350 .map(|recipe| recipe.path().clone())
5351 .collect::<Vec<_>>();
5352 assert_eq!(owners.len(), 2);
5353 for owner in owners {
5354 runtime
5355 .borrow_mut()
5356 .set_component_state_from_host(&owner, "heavy", UiValue::Bool(true))
5357 .unwrap();
5358 }
5359 assert!(lifecycle.render_dirty(&mut engine).is_err());
5360 }
5361
5362 #[test]
5363 fn retained_virtual_renderer_budget_ignores_parent_history() {
5364 let mut measured = Vec::new();
5365 for parent_work in [0, 250_000] {
5366 let mut engine = RuntimeEngine::new();
5367 let source = format!(
5368 r#"
5369 fn render_item(ctx, payload) {{
5370 let n = 0;
5371 if payload.item.heavy {{ for i in 0..100000 {{ n += 1; }} }}
5372 text(`${{n}}`).with_key(payload.key)
5373 }}
5374 fn view(ctx) {{
5375 let n = 0;
5376 for i in 0..{parent_work} {{ n += 1; }}
5377 let data = [];
5378 for index in 0..32 {{
5379 data.push(#{{ key: `row-${{index}}`, heavy: index == 31 }});
5380 }}
5381 virtual_collection(#{{
5382 key: "rows", label: "Rows", data: data,
5383 estimated_height: 24, height: 24, overdraw_pixels: 0,
5384 alignment: "top", follow_tail: false,
5385 }}, Fn("render_item"))
5386 }}
5387 "#
5388 );
5389 let compiled = engine.compile_named("virtual-budget", &source).unwrap();
5390 let runtime = Rc::new(RefCell::new(crate::UiRuntimeState::new()));
5391 let root_path = ComponentInstancePath::root("App", "budget");
5392 let mut lifecycle = crate::ScriptLifecycle::new(
5393 compiled,
5394 Rc::clone(&runtime),
5395 root_path.clone(),
5396 None,
5397 BTreeMap::new(),
5398 &ComponentStateSchema::default(),
5399 )
5400 .unwrap();
5401 lifecycle.start(&mut engine).unwrap();
5402 let id = engine
5403 .virtual_collection_ids_in_scope(&root_path)
5404 .into_iter()
5405 .next()
5406 .unwrap();
5407 let _ = engine.take_timings();
5408 runtime.borrow().virtual_requests.request(id, [31]);
5409 assert!(lifecycle.realize_virtual_requests(&mut engine).unwrap());
5410 let operations = engine
5411 .take_timings()
5412 .into_iter()
5413 .filter(|timing| {
5414 matches!(timing.operation, ExecutionOperation::VirtualCollection(_))
5415 })
5416 .map(|timing| timing.operations)
5417 .sum::<u64>();
5418 assert!(
5419 operations < DEFAULT_SCRIPT_OPERATION_LIMIT / 2,
5420 "{operations}"
5421 );
5422 measured.push(operations);
5423 }
5424 assert_eq!(measured[0], measured[1]);
5425 }
5426
5427 #[test]
5428 fn retained_virtual_renderer_replays_curried_node_presentation() {
5429 let mut engine = RuntimeEngine::new();
5430 let compiled = engine
5431 .compile_named(
5432 "virtual-rich-presentation",
5433 r#"
5434 fn render_item(content, ctx, payload) {
5435 content[payload.key]
5436 }
5437 fn view(ctx) {
5438 let data = [];
5439 let content = #{};
5440 for index in 0..32 {
5441 let key = `row-${index}`;
5442 data.push(#{ key: key });
5443 content[key] = text(`Rich ${index}`);
5444 }
5445 virtual_collection(#{
5446 key: "rows", label: "Rows", data: data,
5447 estimated_height: 24, height: 24, overdraw_pixels: 0,
5448 alignment: "top", follow_tail: false,
5449 }, Fn("render_item").curry(content))
5450 }
5451 "#,
5452 )
5453 .unwrap();
5454 let runtime = Rc::new(RefCell::new(crate::UiRuntimeState::new()));
5455 let root_path = ComponentInstancePath::root("App", "rich");
5456 let mut lifecycle = crate::ScriptLifecycle::new(
5457 compiled,
5458 Rc::clone(&runtime),
5459 root_path.clone(),
5460 None,
5461 BTreeMap::new(),
5462 &ComponentStateSchema::default(),
5463 )
5464 .unwrap();
5465 lifecycle.start(&mut engine).unwrap();
5466 let id = engine
5467 .virtual_collection_ids_in_scope(&root_path)
5468 .into_iter()
5469 .next()
5470 .unwrap();
5471 runtime.borrow().virtual_requests.request(id, [31]);
5472 assert!(lifecycle.realize_virtual_requests(&mut engine).unwrap());
5473 let crate::UiNodeKind::VirtualCollection { spec } = lifecycle.root().unwrap().kind() else {
5474 unreachable!()
5475 };
5476 assert!(matches!(
5477 spec.realized[&31].kind(),
5478 crate::UiNodeKind::Text { text } if text == "Rich 31"
5479 ));
5480 }
5481
5482 #[test]
5483 #[allow(clippy::too_many_lines)]
5484 fn virtual_row_formal_component_preserves_the_real_callback_owner() {
5485 let mut engine = RuntimeEngine::new();
5486 let compiled = engine
5487 .compile_named(
5488 "virtual-callback-owner",
5489 r#"
5490 define_component(#{
5491 metadata: #{ id: "test/action", "export": "Action", version: "0.1.8",
5492 runtime_api: #{ min_inclusive: 2, max_exclusive: 3 },
5493 dependencies: [], capabilities: #{} },
5494 schema: #{ props: #{
5495 key: #{ schema: #{ type: "string" }, required: true, sensitive: false },
5496 on_action: #{ schema: #{ type: "callback" }, required: true, sensitive: false },
5497 }, state: #{ fields: #{} }, events: #{}, slots: #{}, parts: [] },
5498 render: Fn("render_Action")
5499 });
5500 fn Action(props) { render_component("test/action", props) }
5501 fn render_Action(ctx, props) { text("Action").on_click(props.on_action) }
5502 fn increment(ctx, payload) { ctx.set_state("count", ctx.get_state("count") + 1); }
5503 fn render_item(ctx, payload) {
5504 Action(#{ key: payload.key, on_action: Fn("increment") })
5505 }
5506 fn view(ctx) {
5507 let data=[];for index in 0..32 {data.push(#{key:`row-${index}`});}
5508 virtual_collection(#{
5509 key: "rows", label: "Rows", data: data,
5510 estimated_height: 24, height: 24, overdraw_pixels: 0,
5511 alignment: "top", follow_tail: false,
5512 }, Fn("render_item"))
5513 }
5514 "#,
5515 )
5516 .unwrap();
5517 let runtime = Rc::new(RefCell::new(crate::UiRuntimeState::new()));
5518 let root_path = ComponentInstancePath::root("App", "virtual-callback");
5519 let schema = ComponentStateSchema::new(BTreeMap::from([(
5520 "count".to_owned(),
5521 crate::StateField::new(
5522 crate::ValueSchema::Integer {
5523 min: None,
5524 max: None,
5525 },
5526 UiValue::Integer(0),
5527 ),
5528 )]))
5529 .unwrap();
5530 let mut lifecycle = crate::ScriptLifecycle::new(
5531 compiled,
5532 Rc::clone(&runtime),
5533 root_path.clone(),
5534 None,
5535 BTreeMap::new(),
5536 &schema,
5537 )
5538 .unwrap();
5539 lifecycle.start(&mut engine).unwrap();
5540 let crate::UiNodeKind::VirtualCollection { spec } = lifecycle.root().unwrap().kind() else {
5541 unreachable!()
5542 };
5543 let crate::UiEventHandler::Script(callback) =
5544 spec.realized[&0].handlers()["click"][0].handler()
5545 else {
5546 panic!("formal row component must retain its callback");
5547 };
5548 let _ = lifecycle
5549 .invoke_callback_transactional(&engine, callback, UiValue::Null)
5550 .unwrap();
5551 assert_eq!(
5552 runtime.borrow().component_state.get(&root_path, "count"),
5553 Some(&UiValue::Integer(1))
5554 );
5555 let id = engine
5556 .virtual_collection_ids_in_scope(&root_path)
5557 .into_iter()
5558 .next()
5559 .unwrap();
5560 assert!(
5561 !lifecycle
5562 .root()
5563 .unwrap()
5564 .virtual_collection_items(&id)
5565 .unwrap()
5566 .contains_key(&31)
5567 );
5568 runtime.borrow().virtual_requests.request(id.clone(), [31]);
5569 lifecycle.realize_virtual_requests(&mut engine).unwrap();
5570 let callback = lifecycle
5571 .root()
5572 .unwrap()
5573 .virtual_collection_items(&id)
5574 .unwrap()[&31]
5575 .handler("click")
5576 .unwrap()
5577 .as_script()
5578 .unwrap();
5579 let _ = lifecycle
5580 .invoke_callback_transactional(&engine, callback, UiValue::Null)
5581 .unwrap();
5582 assert_eq!(
5583 runtime.borrow().component_state.get(&root_path, "count"),
5584 Some(&UiValue::Integer(2))
5585 );
5586 }
5587
5588 #[test]
5589 fn invalid_state_default_reports_the_field_and_tagged_encoding() {
5590 let mut engine = RuntimeEngine::new();
5591 let compiled = engine.compile(r#"
5592 fn state_schema() {#{fields:#{current_theme:#{schema:#{type:"map",values:#{type:"ui_value"}},
5593 "default":#{type:"map",value:#{family:"Default"}}}}}}
5594 fn view(ctx) {text("view")}
5595 "#).unwrap();
5596 let error = engine.root_state_schema(&compiled).unwrap_err().to_string();
5597 assert!(
5598 error.contains("state_schema.fields.current_theme.default"),
5599 "{error}"
5600 );
5601 assert!(error.contains("recursively tagged UiValue"), "{error}");
5602 }
5603
5604 #[test]
5605 fn delayed_component_callback_starts_with_a_fresh_operation_budget() {
5606 let module = r#"
5607 define_component(#{
5608 metadata: #{ id: "components/counter", "export": "Counter", version: "0.1.1",
5609 runtime_api: #{ min_inclusive: 2, max_exclusive: 3 }, dependencies: [], capabilities: #{} },
5610 schema: #{ props: #{}, state: #{ fields: #{} }, events: #{}, slots: #{}, parts: ["root"] },
5611 render: Fn("render_Counter")
5612 });
5613 fn Counter(props) { render_component("components/counter", props) }
5614 fn render_Counter(ctx, props) { text("counter").on_click(Fn("clicked")) }
5615 fn clicked(ctx, payload) {
5616 let n = 0;
5617 for i in 0..100000 { n += 1; }
5618 n
5619 }
5620 "#;
5621 let mut resolver = crate::RestrictedModuleResolver::new();
5622 resolver.insert("components/counter", module).unwrap();
5623 let mut engine = RuntimeEngine::new();
5624 engine.set_module_resolver(resolver);
5625 let compiled = engine
5626 .compile_self_contained_named(
5627 "callback-budget",
5628 r#"import "components/counter" as c;
5629 fn view(ctx) {
5630 let n = 0;
5631 for i in 0..250000 { n += 1; }
5632 c::Counter(#{} )
5633 }"#,
5634 )
5635 .unwrap();
5636 let runtime = Rc::new(RefCell::new(UiRuntimeState::new()));
5637 let mut lifecycle = crate::ScriptLifecycle::new(
5638 compiled,
5639 runtime,
5640 ComponentInstancePath::root("App", "budget"),
5641 None,
5642 BTreeMap::new(),
5643 &ComponentStateSchema::default(),
5644 )
5645 .unwrap();
5646 let root = lifecycle.start(&mut engine).unwrap();
5647 let crate::UiEventHandler::Script(callback) = root.handlers()["click"][0].handler() else {
5648 panic!("component must expose a script click callback");
5649 };
5650 let callback = callback.clone();
5651 assert!(
5652 lifecycle
5653 .invoke_callback_transactional(&engine, &callback, UiValue::Null)
5654 .is_ok()
5655 );
5656 }
5657
5658 #[test]
5659 fn old_component_handles_cannot_target_a_same_key_remount() {
5660 let module = r#"
5661 define_component(#{
5662 metadata: #{ id: "components/incarnation", "export": "Counter", version: "0.1.1",
5663 runtime_api: #{ min_inclusive: 2, max_exclusive: 3 }, dependencies: [], capabilities: #{} },
5664 schema: #{ props: #{ key: #{ schema: #{ type: "string" }, required: true, sensitive: false } },
5665 state: #{ fields: #{ count: #{ schema: #{ type: "integer" }, "default": #{ type: "integer", value: 0 } } } },
5666 events: #{}, slots: #{}, parts: ["root"] }, render: Fn("render_Counter")
5667 });
5668 fn Counter(props) { render_component("components/incarnation", props) }
5669 fn increment(ctx, payload) { ctx.set_state("count", ctx.get_state("count") + 1); }
5670 fn render_Counter(ctx, props) {
5671 let progress = signal("progress", 0.0);
5672 text(`${ctx.get_state("count")}`).bind_signal("opacity", progress).on_click(Fn("increment"))
5673 }
5674 "#;
5675 let mut resolver = crate::RestrictedModuleResolver::new();
5676 resolver.insert("components/incarnation", module).unwrap();
5677 let mut engine = RuntimeEngine::new();
5678 engine.set_module_resolver(resolver);
5679 let compiled = engine
5680 .compile_self_contained_named(
5681 "incarnation",
5682 r#"import "components/incarnation" as c;
5683 fn view(ctx) {
5684 if ctx.get_state("visible") { c::Counter(#{ key: "same" }) }
5685 else { text("hidden") }
5686 }"#,
5687 )
5688 .unwrap();
5689 let runtime = Rc::new(RefCell::new(UiRuntimeState::new()));
5690 let root_path = ComponentInstancePath::root("App", "incarnation");
5691 let schema = ComponentStateSchema::new(BTreeMap::from([(
5692 "visible".to_owned(),
5693 crate::StateField::new(crate::ValueSchema::Bool, UiValue::Bool(true)),
5694 )]))
5695 .unwrap();
5696 let mut lifecycle = crate::ScriptLifecycle::new(
5697 compiled,
5698 Rc::clone(&runtime),
5699 root_path.clone(),
5700 None,
5701 BTreeMap::new(),
5702 &schema,
5703 )
5704 .unwrap();
5705 lifecycle.start(&mut engine).unwrap();
5706 let component = engine
5707 .component_invocations()
5708 .next()
5709 .unwrap()
5710 .path()
5711 .clone();
5712 let crate::UiEventHandler::Script(old_callback) =
5713 lifecycle.root().unwrap().handlers()["click"][0].handler()
5714 else {
5715 panic!("component must expose its callback");
5716 };
5717 let old_callback = old_callback.clone();
5718 let old_signal = runtime
5719 .borrow()
5720 .signals
5721 .resolve(&component, "progress")
5722 .unwrap();
5723 runtime
5724 .borrow_mut()
5725 .set_component_state_from_host(&root_path, "visible", UiValue::Bool(false))
5726 .unwrap();
5727 lifecycle.render_dirty(&mut engine).unwrap();
5728 runtime
5729 .borrow_mut()
5730 .set_component_state_from_host(&root_path, "visible", UiValue::Bool(true))
5731 .unwrap();
5732 lifecycle.render_dirty(&mut engine).unwrap();
5733 assert!(matches!(
5734 lifecycle.invoke_callback_transactional(&engine, &old_callback, UiValue::Null),
5735 Err(crate::LifecycleError::Runtime(
5736 RuntimeError::StaleComponentCallback { .. }
5737 ))
5738 ));
5739 assert!(matches!(
5740 runtime
5741 .borrow_mut()
5742 .signals
5743 .write(&old_signal, crate::SignalValue::Float(0.5)),
5744 Err(crate::SignalError::Stale(_))
5745 ));
5746 }
5747}