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