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