1use std::cell::RefCell;
2use std::collections::{BTreeMap, BTreeSet};
3use std::rc::Rc;
4use std::sync::atomic::{AtomicU64, Ordering};
5use std::time::Duration;
6
7use rhai::{
8 Array, CustomType, Dynamic, Engine, EvalAltResult, FLOAT, FnPtr, INT, ImmutableString, Map,
9 NativeCallContext, Position, TypeBuilder,
10};
11use thiserror::Error;
12
13use crate::{
14 ActionError, ActionId, ActionInvocation, ActionRegistry, AnimationKey, AnimationRuntime,
15 AssetError, AssetId, AssetRegistry, AsyncRuntimeError, AsyncScope, CalendarClock,
16 CapabilityError, CapabilityId, CapabilityRegistry, ComponentInstancePath, DateStyle,
17 EventSchema, ImageDecodeHandle, LocaleError, LocaleManager, NumberFormatOptions, OpaqueHandle,
18 ResponsiveError, ResponsiveRuntime, ScriptCallback, ScriptGeneration, ScriptWindowSpec,
19 StateError, StateStore, StoreError, StoreId, StoreRegistry, SubscriptionCloseReason,
20 SubscriptionDeliveryPolicy, SubscriptionHandle, SubscriptionOptions, SubscriptionRegistration,
21 SubscriptionRegistry, TaskHandle, TaskRegistry, TextDirection, ThemeError, ThemeManager,
22 ThemePreference, ThemeSelection, UiEvent, UiValue, UiValueError, UiValuePath, UiValuePathError,
23 UiValuePathSegment, WindowCommandError, WindowCommandRegistry,
24};
25
26#[derive(Clone, Copy, Debug, Eq, PartialEq)]
27pub enum ExecutionPhase {
28 Init,
29 Render,
30 Event,
31 Suspend,
32 Resume,
33 Dispose,
34}
35
36#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
39pub struct ComponentIncarnation(u64);
40
41impl ComponentIncarnation {
42 pub(crate) const fn unscoped() -> Self {
43 Self(0)
44 }
45
46 #[must_use]
47 pub const fn get(self) -> u64 {
48 self.0
49 }
50}
51
52#[derive(Clone, Copy, Debug, Eq, PartialEq)]
53struct RuntimeStateId(u64);
54
55impl Default for RuntimeStateId {
56 fn default() -> Self {
57 static NEXT_ID: AtomicU64 = AtomicU64::new(1);
58 Self(NEXT_ID.fetch_add(1, Ordering::Relaxed))
59 }
60}
61
62enum ThemeTarget {
63 App,
64 Window(String),
65 Local,
66}
67
68impl ExecutionPhase {
69 const fn allows_mutation(self) -> bool {
70 matches!(
71 self,
72 Self::Init | Self::Event | Self::Suspend | Self::Resume | Self::Dispose
73 )
74 }
75
76 const fn allows_async_start(self) -> bool {
77 matches!(self, Self::Init | Self::Event | Self::Resume)
78 }
79}
80
81#[derive(Debug, Default)]
82pub struct UiRuntimeState {
83 runtime_id: RuntimeStateId,
84 transaction_depth: usize,
85 pub component_state: StateStore,
86 pub stores: StoreRegistry,
87 pub native_collections: crate::NativeCollectionRegistry,
88 pub native_documents: crate::NativeTextDocumentRegistry,
89 pub actions: ActionRegistry,
90 pub capabilities: CapabilityRegistry,
91 pub tasks: TaskRegistry,
92 pub subscriptions: SubscriptionRegistry,
93 pub timers: crate::TimerRegistry,
94 pub locale: Option<LocaleManager>,
95 pub calendar_clock: CalendarClock,
96 pub clock: crate::RuntimeClock,
97 pub theme: Option<ThemeManager>,
98 component_styles: crate::ComponentStyleSheet,
99 pub assets: AssetRegistry,
100 pub animations: AnimationRuntime,
101 pub effects: crate::EffectRegistry,
102 pub signals: crate::SignalRegistry,
103 pub element_refs: crate::ElementRefRegistry,
104 pub(crate) geometry: crate::GeometryRegistry,
105 pub(crate) pointer_capture: crate::PointerCaptureRegistry,
106 presentations: BTreeMap<String, ViewPresentationState>,
107 pub budgets: crate::RuntimeBudgets,
108 pub virtual_requests: crate::VirtualRequestRegistry,
109 pub windows: WindowCommandRegistry,
110 pub responsive: ResponsiveRuntime,
111 pub(crate) environment_dependencies:
112 crate::environment_dependency::EnvironmentDependencyRegistry,
113 pub animation_values: BTreeMap<AnimationKey, f64>,
114 pub traces: crate::TraceBuffer,
115 component_event_handlers: BTreeMap<(ComponentInstancePath, String), ScriptCallback>,
116 component_incarnations: BTreeMap<ComponentInstancePath, ComponentIncarnation>,
117 next_component_incarnation: u64,
118 dirty: BTreeSet<ComponentInstancePath>,
119 pending_events: Vec<PendingEvent>,
120 pending_actions: Vec<ActionInvocation>,
121 pending_async: Vec<crate::AsyncDelivery>,
122 pending_element_commands: Vec<crate::element_ref::ElementCommand>,
123 repaint_windows: BTreeSet<String>,
124 component_style_generation: u64,
125}
126
127impl UiRuntimeState {
128 const SUSPENDED_DELIVERY_CAPACITY: usize = 256;
129 #[must_use]
130 pub fn new() -> Self {
131 Self::default()
132 }
133
134 pub(crate) fn ensure_component_incarnation(
135 &mut self,
136 component: &ComponentInstancePath,
137 ) -> ComponentIncarnation {
138 if let Some(incarnation) = self.component_incarnations.get(component) {
139 return *incarnation;
140 }
141 let next = self.next_component_incarnation.max(1);
142 let incarnation = ComponentIncarnation(next);
143 self.next_component_incarnation = next.saturating_add(1);
144 self.component_incarnations
145 .insert(component.clone(), incarnation);
146 incarnation
147 }
148
149 pub(crate) fn component_incarnation(
150 &self,
151 component: &ComponentInstancePath,
152 ) -> Option<ComponentIncarnation> {
153 self.component_incarnations.get(component).copied()
154 }
155
156 pub(crate) fn ensure_presentation(&mut self, view: &str) {
157 self.presentations.entry(view.to_owned()).or_default();
158 }
159
160 pub fn presentation_geometry(&mut self, view: &str) -> crate::GeometryRegistry {
162 self.ensure_presentation(view);
163 self.geometry_for(Some(view))
164 }
165
166 pub fn presentation_pointer_capture(&mut self, view: &str) -> crate::PointerCaptureRegistry {
168 self.ensure_presentation(view);
169 self.pointer_capture_for(Some(view))
170 }
171
172 pub(crate) fn geometry_for(&self, view: Option<&str>) -> crate::GeometryRegistry {
173 view.and_then(|view| self.presentations.get(view))
174 .map_or_else(|| self.geometry.clone(), |state| state.geometry.clone())
175 }
176
177 pub(crate) fn pointer_capture_for(&self, view: Option<&str>) -> crate::PointerCaptureRegistry {
178 view.and_then(|view| self.presentations.get(view))
179 .map_or_else(
180 || self.pointer_capture.clone(),
181 |state| state.pointer_capture.clone(),
182 )
183 }
184
185 pub(crate) fn remove_presentation(&mut self, view: &str) {
186 self.presentations.remove(view);
187 }
188
189 pub fn set_component_state_from_host(
196 &mut self,
197 component: &ComponentInstancePath,
198 field: &str,
199 value: UiValue,
200 ) -> Result<bool, crate::StateError> {
201 let changed = self.component_state.set(component, field, value)?;
202 if changed {
203 self.dirty.insert(component.clone());
204 }
205 Ok(changed)
206 }
207
208 pub fn replace_native_collection_from_host(
214 &mut self,
215 name: &str,
216 collection: crate::NativeCollection,
217 ) -> Result<bool, crate::NativeCollectionError> {
218 let invalidated = self.native_collections.replace(name, collection)?;
219 let changed = !invalidated.is_empty();
220 self.dirty.extend(invalidated);
221 Ok(changed)
222 }
223
224 pub fn replace_native_text_document_from_host(
230 &mut self,
231 name: &str,
232 document: crate::NativeTextDocument,
233 ) -> Result<bool, crate::DocumentError> {
234 let invalidated = self.native_documents.replace(name, document)?;
235 let changed = !invalidated.is_empty();
236 self.dirty.extend(invalidated);
237 Ok(changed)
238 }
239
240 pub fn replace_theme_variant_from_host(
247 &mut self,
248 variant: crate::ThemeVariant,
249 ) -> Result<(), crate::ThemeError> {
250 self.theme
251 .as_mut()
252 .ok_or(crate::ThemeError::NoVariants)?
253 .replace_variant(variant)?;
254 self.dirty.extend(self.component_state.paths());
255 Ok(())
256 }
257
258 pub fn replace_component_styles_from_host(
263 &mut self,
264 styles: crate::ComponentStyleSheet,
265 ) -> bool {
266 if self.component_styles == styles {
267 return false;
268 }
269 self.component_styles = styles;
270 self.component_style_generation = self.component_style_generation.saturating_add(1);
271 self.dirty.extend(self.component_state.paths());
272 true
273 }
274
275 pub(crate) const fn component_style_generation(&self) -> u64 {
276 self.component_style_generation
277 }
278
279 pub(crate) const fn component_styles(&self) -> &crate::ComponentStyleSheet {
280 &self.component_styles
281 }
282
283 pub(crate) fn trace_subscription_closures(&mut self) {
284 for closure in self.subscriptions.take_closures() {
285 let scope = match closure.scope {
286 AsyncScope::App => "/App".to_owned(),
287 AsyncScope::Window(window) => format!("window:{window}"),
288 AsyncScope::Component(component) => component.to_string(),
289 AsyncScope::Effect {
290 component,
291 key,
292 activation,
293 } => format!("{component}/effect[{key}]#{activation}"),
294 };
295 self.traces.push(
296 crate::RuntimeTraceKind::Subscription,
297 scope,
298 format!("close {}: {}", closure.label, closure.reason),
299 None,
300 false,
301 );
302 }
303 }
304
305 pub fn release_window(
311 &mut self,
312 window: &str,
313 root: &ComponentInstancePath,
314 ) -> Result<(), AssetError> {
315 self.tasks
316 .cancel_scope(&AsyncScope::Window(window.to_owned()));
317 self.tasks.cancel_component_scope(root);
318 self.timers.cancel_component_scope(root);
319 self.subscriptions
320 .cancel_scope(&AsyncScope::Window(window.to_owned()));
321 self.subscriptions.cancel_component_scope(root);
322 self.assets.cancel_window_scope(window, root)?;
323 self.stores.remove_window(window);
324 self.native_collections.remove_reader_scope(root);
325 self.native_documents.remove_reader_scope(root);
326 self.component_state.remove_scope(root);
327 self.component_event_handlers
328 .retain(|(path, _), _| !path.is_within(root));
329 self.component_incarnations
330 .retain(|path, _| !path.is_within(root));
331 self.actions.remove_component_scope(root);
332 self.dirty.retain(|path| !path.is_within(root));
333 self.pending_events
334 .retain(|event| !event.target.is_within(root));
335 self.pending_actions.retain(|action| {
336 !action
337 .callback
338 .component()
339 .is_some_and(|path| path.is_within(root))
340 });
341 self.pending_async.retain(|delivery| match &delivery.scope {
342 AsyncScope::App => true,
343 AsyncScope::Window(id) => id != window,
344 AsyncScope::Component(_) | AsyncScope::Effect { .. } => {
345 !delivery.scope.is_within_component(root)
346 }
347 });
348 self.pending_element_commands
349 .retain(|command| command.window() != window);
350 self.environment_dependencies.remove_window(window);
351 self.environment_dependencies.remove_scope(root);
352 self.repaint_windows.remove(window);
353 if let Some(theme) = self.theme.as_mut() {
354 theme.remove_window(window);
355 theme.remove_scope(root);
356 }
357 if let Some(locale) = self.locale.as_mut() {
358 locale.remove_window(window);
359 locale.remove_scope(root);
360 }
361 self.animations
362 .cancel_node_scope(&format!("window:{window}"));
363 self.animation_values = self.animations.snapshot(self.clock.now());
364 self.effects.remove_scope(root);
365 self.signals.remove_scope(root);
366 self.element_refs.remove_scope(root);
367 self.windows.remove(window);
368 self.responsive.remove_window(window);
369 Ok(())
370 }
371
372 pub(crate) fn queue_async(
373 &mut self,
374 deliveries: impl IntoIterator<Item = crate::AsyncDelivery>,
375 ) {
376 self.pending_async.extend(deliveries);
377 }
378
379 pub(crate) fn queue_suspended_async(
380 &mut self,
381 deliveries: impl IntoIterator<Item = crate::AsyncDelivery>,
382 ) -> Result<(), AsyncRuntimeError> {
383 for delivery in deliveries {
384 if self.pending_async.len() >= Self::SUSPENDED_DELIVERY_CAPACITY {
385 return Err(AsyncRuntimeError::SuspendedBackpressure {
386 capacity: Self::SUSPENDED_DELIVERY_CAPACITY,
387 });
388 }
389 self.pending_async.push(delivery);
390 }
391 Ok(())
392 }
393
394 pub(crate) fn suspended_delivery_capacity_remaining(&self) -> usize {
395 Self::SUSPENDED_DELIVERY_CAPACITY.saturating_sub(self.pending_async.len())
396 }
397
398 pub(crate) fn cancel_async_scope(&mut self, scope: &AsyncScope) -> Result<(), AssetError> {
399 self.assets.cancel_scope(scope)?;
400 self.tasks.cancel_scope(scope);
401 self.subscriptions.cancel_scope(scope);
402 self.pending_async
403 .retain(|delivery| &delivery.scope != scope);
404 self.trace_subscription_closures();
405 Ok(())
406 }
407
408 pub(crate) fn take_window_async(
409 &mut self,
410 window: &str,
411 root: &ComponentInstancePath,
412 ) -> Vec<crate::AsyncDelivery> {
413 let app_owner = self.windows.open_ids().into_iter().next();
414 let pending = std::mem::take(&mut self.pending_async);
415 let (accepted, retained) =
416 pending
417 .into_iter()
418 .partition(|delivery| match &delivery.scope {
419 AsyncScope::App => app_owner.as_deref() == Some(window),
420 AsyncScope::Window(id) => id == window,
421 AsyncScope::Component(_) | AsyncScope::Effect { .. } => {
422 delivery.scope.is_within_component(root)
423 }
424 });
425 self.pending_async = retained;
426 accepted
427 }
428
429 pub(crate) fn discard_component_async_before_generation(
430 &mut self,
431 root: &ComponentInstancePath,
432 generation: crate::ScriptGeneration,
433 ) {
434 self.pending_async.retain(|delivery| {
435 !delivery.scope.is_within_component(root)
436 || delivery.callback.generation() == generation
437 });
438 }
439
440 pub(crate) fn take_window_element_commands(
441 &mut self,
442 window: &str,
443 ) -> Vec<crate::element_ref::ElementCommand> {
444 let commands = std::mem::take(&mut self.pending_element_commands);
445 let (selected, retained) = commands
446 .into_iter()
447 .partition(|command| command.window() == window);
448 self.pending_element_commands = retained;
449 selected
450 }
451
452 pub(crate) fn has_window_dirty(&self, root: &ComponentInstancePath) -> bool {
453 self.dirty.iter().any(|path| path.is_within(root))
454 }
455
456 pub(crate) fn flush_geometry_dependencies(&mut self) {
457 self.dirty.extend(self.geometry.take_dirty());
458 for presentation in self.presentations.values() {
459 self.dirty.extend(presentation.geometry.take_dirty());
460 }
461 }
462
463 pub(crate) fn take_window_dirty_components(
464 &mut self,
465 root: &ComponentInstancePath,
466 ) -> BTreeSet<ComponentInstancePath> {
467 let all = std::mem::take(&mut self.dirty);
468 let (selected, retained) = all.into_iter().partition(|path| path.is_within(root));
469 self.dirty = retained;
470 selected
471 }
472
473 pub(crate) fn drain_pending_actions(&mut self) -> Vec<ActionInvocation> {
474 std::mem::take(&mut self.pending_actions)
475 }
476
477 pub(crate) fn drain_pending_events(&mut self) -> Vec<PendingEvent> {
478 std::mem::take(&mut self.pending_events)
479 }
480
481 pub(crate) fn has_pending_dispatch(&self) -> bool {
482 !self.pending_actions.is_empty() || !self.pending_events.is_empty()
483 }
484
485 pub(crate) fn has_virtual_requests(&self) -> bool {
486 !self.virtual_requests.is_empty()
487 }
488
489 pub(crate) fn component_event_handler(
490 &self,
491 component: &ComponentInstancePath,
492 event: &str,
493 ) -> Option<ScriptCallback> {
494 self.component_event_handlers
495 .get(&(component.clone(), event.to_owned()))
496 .cloned()
497 }
498
499 pub(crate) fn reset_component_readers(&mut self, component: &ComponentInstancePath) {
500 self.stores.reset_reader(component);
501 self.native_collections.reset_reader(component);
502 self.native_documents.reset_reader(component);
503 self.environment_dependencies.reset_reader(component);
504 }
505
506 pub(crate) fn component_event_handlers_in_scope(
507 &self,
508 root: &ComponentInstancePath,
509 ) -> BTreeMap<(ComponentInstancePath, String), ScriptCallback> {
510 self.component_event_handlers
511 .iter()
512 .filter(|((path, _), _)| path.is_within(root))
513 .map(|(id, callback)| (id.clone(), callback.clone()))
514 .collect()
515 }
516
517 pub(crate) fn replace_component_event_handlers(
518 &mut self,
519 root: &ComponentInstancePath,
520 handlers: BTreeMap<(ComponentInstancePath, String), ScriptCallback>,
521 ) {
522 self.component_event_handlers
523 .retain(|(path, _), _| !path.is_within(root));
524 self.component_event_handlers.extend(handlers);
525 }
526
527 pub(crate) fn mark_all_windows_dirty(&mut self) {
528 self.dirty.extend(
529 self.windows
530 .open_ids()
531 .into_iter()
532 .map(|window| ComponentInstancePath::root("App", window)),
533 );
534 }
535
536 pub(crate) fn mark_dirty(
537 &mut self,
538 components: impl IntoIterator<Item = ComponentInstancePath>,
539 ) {
540 self.dirty.extend(components);
541 }
542
543 pub(crate) fn mark_all_windows_repaint(&mut self) {
544 self.repaint_windows.extend(self.windows.open_ids());
545 }
546
547 pub(crate) fn mark_window_repaint(&mut self, window: impl Into<String>) {
548 self.repaint_windows.insert(window.into());
549 }
550
551 pub(crate) fn take_window_repaint(&mut self, window: &str) -> bool {
552 self.repaint_windows.remove(window)
553 }
554
555 pub(crate) fn reconcile_component_lifetimes(
556 &mut self,
557 root: &ComponentInstancePath,
558 active: &BTreeSet<ComponentInstancePath>,
559 previous: &BTreeSet<ComponentInstancePath>,
560 ) -> Result<(), AssetError> {
561 for removed in previous
562 .iter()
563 .filter(|path| path.is_within(root) && *path != root && !active.contains(*path))
564 {
565 self.tasks.cancel_component_scope(removed);
566 self.subscriptions.cancel_component_scope(removed);
567 self.timers.cancel_component_scope(removed);
568 self.assets.cancel_component_scope(removed)?;
569 self.actions.remove_component_scope(removed);
570 }
571 self.pending_async.retain(|delivery| {
572 delivery
573 .scope
574 .component()
575 .is_none_or(|path| !path.is_within(root) || active.contains(path))
576 });
577 self.pending_actions.retain(|action| {
578 !action
579 .callback
580 .component()
581 .is_some_and(|path| path.is_within(root) && !active.contains(path))
582 });
583 self.pending_events.retain(|event| {
584 !event.target.is_within(root) || active.contains(&event.target) || event.target == *root
585 });
586 self.stores.retain_reader_scope(root, active);
587 self.native_collections.retain_reader_scope(root, active);
588 self.native_documents.retain_reader_scope(root, active);
589 self.environment_dependencies.retain_scope(root, active);
590 self.dirty
591 .retain(|path| !path.is_within(root) || path == root || active.contains(path));
592 self.component_incarnations
593 .retain(|path, _| !path.is_within(root) || path == root || active.contains(path));
594 Ok(())
595 }
596
597 #[must_use]
598 pub fn drain_batch(&mut self) -> UiMutationBatch {
599 UiMutationBatch {
600 dirty: std::mem::take(&mut self.dirty),
601 events: std::mem::take(&mut self.pending_events),
602 actions: std::mem::take(&mut self.pending_actions),
603 }
604 }
605
606 #[must_use]
607 pub fn dirty_components(&self) -> &BTreeSet<ComponentInstancePath> {
608 &self.dirty
609 }
610
611 pub(crate) fn snapshot(&self) -> Result<UiStateSnapshot, AssetError> {
612 Ok(UiStateSnapshot {
613 runtime_id: self.runtime_id,
614 transaction_depth: self.transaction_depth,
615 component_state: self.component_state.clone(),
616 stores: self.stores.clone(),
617 native_collections: self.native_collections.clone(),
618 native_documents: self.native_documents.clone(),
619 actions: self.actions.clone(),
620 component_event_handlers: self.component_event_handlers.clone(),
621 component_incarnations: self.component_incarnations.clone(),
622 next_component_incarnation: self.next_component_incarnation,
623 dirty: self.dirty.clone(),
624 pending_events: self.pending_events.clone(),
625 pending_actions: self.pending_actions.clone(),
626 pending_async: self.pending_async.clone(),
627 pending_element_commands: self.pending_element_commands.clone(),
628 locale: self.locale.clone(),
629 theme: self.theme.clone(),
630 component_styles: self.component_styles.clone(),
631 component_style_generation: self.component_style_generation,
632 animations: self.animations.clone(),
633 effects: self.effects.clone(),
634 signals: self.signals.clone(),
635 element_refs: self.element_refs.clone(),
636 geometry: self.geometry.snapshot(),
637 pointer_capture: self.pointer_capture.snapshot(),
638 presentations: self
639 .presentations
640 .iter()
641 .map(|(view, state)| (view.clone(), state.snapshot()))
642 .collect(),
643 budgets: self.budgets.clone(),
644 virtual_requests: self.virtual_requests.snapshot(),
645 animation_values: self.animation_values.clone(),
646 windows: self.windows.clone(),
647 responsive: self.responsive.clone(),
648 environment_dependencies: self.environment_dependencies.clone(),
649 repaint_windows: self.repaint_windows.clone(),
650 tasks: self.tasks.snapshot(),
651 subscriptions: self.subscriptions.snapshot(),
652 timers: self.timers.clone(),
653 image_decodes: self.assets.decode_snapshot()?,
654 })
655 }
656
657 pub(crate) fn restore(&mut self, snapshot: UiStateSnapshot) -> Result<(), UiTransactionError> {
658 if self.runtime_id != snapshot.runtime_id {
659 return Err(UiTransactionError::ForeignSnapshot);
660 }
661 let expected_depth = snapshot.transaction_depth.saturating_add(1);
662 if self.transaction_depth != expected_depth {
663 return Err(UiTransactionError::Unbalanced {
664 expected: expected_depth,
665 actual: self.transaction_depth,
666 });
667 }
668 self.tasks.restore(snapshot.tasks);
669 self.subscriptions.restore(snapshot.subscriptions);
670 self.timers = snapshot.timers;
671 let decode_result = self.assets.restore_decode_snapshot(snapshot.image_decodes);
672 self.component_state = snapshot.component_state;
673 self.stores = snapshot.stores;
674 self.native_collections = snapshot.native_collections;
675 self.native_documents = snapshot.native_documents;
676 self.actions = snapshot.actions;
677 self.component_event_handlers = snapshot.component_event_handlers;
678 self.component_incarnations = snapshot.component_incarnations;
679 self.next_component_incarnation = snapshot.next_component_incarnation;
680 self.dirty = snapshot.dirty;
681 self.pending_events = snapshot.pending_events;
682 self.pending_actions = snapshot.pending_actions;
683 self.pending_async = snapshot.pending_async;
684 self.pending_element_commands = snapshot.pending_element_commands;
685 self.locale = snapshot.locale;
686 self.theme = snapshot.theme;
687 self.component_styles = snapshot.component_styles;
688 self.component_style_generation = snapshot.component_style_generation;
689 self.animations = snapshot.animations;
690 self.effects = snapshot.effects;
691 self.signals = snapshot.signals;
692 self.element_refs = snapshot.element_refs;
693 self.geometry.restore(snapshot.geometry);
694 self.pointer_capture.restore(snapshot.pointer_capture);
695 self.presentations
696 .retain(|view, _| snapshot.presentations.contains_key(view));
697 for (view, presentation) in snapshot.presentations {
698 let state = self.presentations.entry(view).or_default();
699 state.geometry.restore(presentation.geometry);
700 state.pointer_capture.restore(presentation.pointer_capture);
701 }
702 self.budgets = snapshot.budgets;
703 self.virtual_requests.restore(snapshot.virtual_requests);
704 self.animation_values = snapshot.animation_values;
705 self.windows = snapshot.windows;
706 self.responsive = snapshot.responsive;
707 self.environment_dependencies = snapshot.environment_dependencies;
708 self.repaint_windows = snapshot.repaint_windows;
709 self.transaction_depth = snapshot.transaction_depth;
710 decode_result.map_err(UiTransactionError::from)
711 }
712
713 pub fn begin_transaction(&mut self) -> Result<UiStateSnapshot, UiTransactionError> {
722 let snapshot = self.snapshot()?;
723 let _ = self.assets.begin_transaction()?;
724 self.tasks.begin_transaction();
725 self.subscriptions.begin_transaction();
726 self.transaction_depth = self.transaction_depth.saturating_add(1);
727 Ok(snapshot)
728 }
729
730 pub fn commit_transaction(&mut self) -> Result<(), UiTransactionError> {
737 if self.transaction_depth == 0 {
738 return Err(UiTransactionError::Unbalanced {
739 expected: 1,
740 actual: 0,
741 });
742 }
743 self.assets.commit_transaction()?;
744 self.tasks.commit_transaction();
745 self.subscriptions.commit_transaction();
746 self.transaction_depth -= 1;
747 Ok(())
748 }
749
750 pub fn rollback_transaction(
757 &mut self,
758 snapshot: UiStateSnapshot,
759 ) -> Result<(), UiTransactionError> {
760 self.restore(snapshot)
761 }
762}
763
764#[derive(Debug, Default)]
765struct ViewPresentationState {
766 geometry: crate::GeometryRegistry,
767 pointer_capture: crate::PointerCaptureRegistry,
768}
769
770impl ViewPresentationState {
771 fn snapshot(&self) -> ViewPresentationSnapshot {
772 ViewPresentationSnapshot {
773 geometry: self.geometry.snapshot(),
774 pointer_capture: self.pointer_capture.snapshot(),
775 }
776 }
777}
778
779#[derive(Clone, Debug)]
780struct ViewPresentationSnapshot {
781 geometry: crate::geometry::GeometrySnapshot,
782 pointer_capture: BTreeMap<u64, crate::NodeId>,
783}
784
785#[derive(Debug, Error)]
786pub enum UiTransactionError {
787 #[error("transaction snapshot belongs to another UiRuntimeState")]
788 ForeignSnapshot,
789 #[error("unbalanced runtime transaction depth: expected {expected}, got {actual}")]
790 Unbalanced { expected: usize, actual: usize },
791 #[error(transparent)]
792 Asset(#[from] AssetError),
793}
794
795#[derive(Clone)]
796pub struct UiStateSnapshot {
797 runtime_id: RuntimeStateId,
798 transaction_depth: usize,
799 component_state: StateStore,
800 stores: StoreRegistry,
801 native_collections: crate::NativeCollectionRegistry,
802 native_documents: crate::NativeTextDocumentRegistry,
803 actions: ActionRegistry,
804 component_event_handlers: BTreeMap<(ComponentInstancePath, String), ScriptCallback>,
805 component_incarnations: BTreeMap<ComponentInstancePath, ComponentIncarnation>,
806 next_component_incarnation: u64,
807 dirty: BTreeSet<ComponentInstancePath>,
808 pending_events: Vec<PendingEvent>,
809 pending_actions: Vec<ActionInvocation>,
810 pending_async: Vec<crate::AsyncDelivery>,
811 pending_element_commands: Vec<crate::element_ref::ElementCommand>,
812 locale: Option<LocaleManager>,
813 theme: Option<ThemeManager>,
814 component_styles: crate::ComponentStyleSheet,
815 component_style_generation: u64,
816 animations: AnimationRuntime,
817 effects: crate::EffectRegistry,
818 signals: crate::SignalRegistry,
819 element_refs: crate::ElementRefRegistry,
820 geometry: crate::geometry::GeometrySnapshot,
821 pointer_capture: BTreeMap<u64, crate::NodeId>,
822 presentations: BTreeMap<String, ViewPresentationSnapshot>,
823 budgets: crate::RuntimeBudgets,
824 virtual_requests: crate::virtual_list::VirtualRequestSnapshot,
825 animation_values: BTreeMap<AnimationKey, f64>,
826 windows: WindowCommandRegistry,
827 responsive: ResponsiveRuntime,
828 environment_dependencies: crate::environment_dependency::EnvironmentDependencyRegistry,
829 repaint_windows: BTreeSet<String>,
830 tasks: crate::async_runtime::TaskRegistrySnapshot,
831 subscriptions: crate::async_runtime::SubscriptionRegistrySnapshot,
832 timers: crate::TimerRegistry,
833 image_decodes: crate::asset::ImageDecodeSnapshot,
834}
835
836impl UiStateSnapshot {
837 pub(crate) const fn component_state(&self) -> &StateStore {
838 &self.component_state
839 }
840}
841
842#[derive(Clone, Debug)]
843pub struct PendingEvent {
844 pub target: ComponentInstancePath,
845 pub event: UiEvent,
846}
847
848#[derive(Clone, Debug, Default)]
849pub struct UiMutationBatch {
850 pub dirty: BTreeSet<ComponentInstancePath>,
851 pub events: Vec<PendingEvent>,
852 pub actions: Vec<ActionInvocation>,
853}
854
855#[derive(Clone, Debug)]
856pub struct UiContext {
857 runtime: Rc<RefCell<UiRuntimeState>>,
858 component: ComponentInstancePath,
859 incarnation: ComponentIncarnation,
860 window: Option<String>,
861 view: Option<String>,
862 phase: ExecutionPhase,
863 events: BTreeMap<String, EventSchema>,
864 generation: ScriptGeneration,
865 native_context: Option<crate::invocation::ScriptInvocationContext>,
866 async_scope: Option<AsyncScope>,
867 component_style: Option<crate::Style>,
868 component_part_styles: BTreeMap<String, crate::Style>,
869 global_component_part_styles: BTreeMap<String, crate::Style>,
870 non_reusable_render_reads: Rc<RefCell<BTreeSet<ComponentInstancePath>>>,
871 event_target: Option<crate::GeometryBounds>,
872}
873
874impl UiContext {
875 #[must_use]
876 pub fn new(
877 runtime: Rc<RefCell<UiRuntimeState>>,
878 component: ComponentInstancePath,
879 window: Option<String>,
880 phase: ExecutionPhase,
881 events: BTreeMap<String, EventSchema>,
882 ) -> Self {
883 if let Some(window) = window.as_deref()
884 && let Ok(mut runtime) = runtime.try_borrow_mut()
885 {
886 runtime.ensure_presentation(window);
887 }
888 let incarnation = runtime
889 .try_borrow_mut()
890 .map(|mut runtime| runtime.ensure_component_incarnation(&component))
891 .unwrap_or_default();
892 let context = Self {
893 runtime,
894 component,
895 incarnation,
896 window,
897 view: None,
898 phase,
899 events,
900 generation: ScriptGeneration::default(),
901 native_context: None,
902 async_scope: None,
903 component_style: None,
904 component_part_styles: BTreeMap::new(),
905 global_component_part_styles: BTreeMap::new(),
906 non_reusable_render_reads: Rc::new(RefCell::new(BTreeSet::new())),
907 event_target: None,
908 };
909 if phase == ExecutionPhase::Render
910 && let Ok(mut runtime) = context.runtime.try_borrow_mut()
911 {
912 runtime.reset_component_readers(&context.component);
913 }
914 context
915 }
916
917 #[must_use]
918 pub fn with_generation(mut self, generation: ScriptGeneration) -> Self {
919 self.generation = generation;
920 self
921 }
922
923 #[must_use]
924 pub fn with_view_id(mut self, view: impl Into<String>) -> Self {
925 let view = view.into();
926 if let Ok(mut runtime) = self.runtime.try_borrow_mut() {
927 runtime.ensure_presentation(&view);
928 }
929 self.view = Some(view);
930 self
931 }
932
933 #[must_use]
934 pub(crate) fn with_optional_view_id(mut self, view: Option<String>) -> Self {
935 if let Some(view) = view.as_deref()
936 && let Ok(mut runtime) = self.runtime.try_borrow_mut()
937 {
938 runtime.ensure_presentation(view);
939 }
940 self.view = view;
941 self
942 }
943
944 pub(crate) fn for_component(
945 &self,
946 component: ComponentInstancePath,
947 events: BTreeMap<String, EventSchema>,
948 ) -> Self {
949 let incarnation = self
950 .runtime
951 .try_borrow_mut()
952 .map(|mut runtime| runtime.ensure_component_incarnation(&component))
953 .unwrap_or_default();
954 let context = Self {
955 runtime: Rc::clone(&self.runtime),
956 component,
957 incarnation,
958 window: self.window.clone(),
959 view: self.view.clone(),
960 phase: self.phase,
961 events,
962 generation: self.generation,
963 native_context: self.native_context.clone(),
964 async_scope: self.async_scope.clone(),
965 component_style: self.component_style.clone(),
966 component_part_styles: self.component_part_styles.clone(),
967 global_component_part_styles: self.global_component_part_styles.clone(),
968 non_reusable_render_reads: Rc::clone(&self.non_reusable_render_reads),
969 event_target: self.event_target,
970 };
971 if context.phase == ExecutionPhase::Render
972 && let Ok(mut runtime) = context.runtime.try_borrow_mut()
973 {
974 runtime.reset_component_readers(&context.component);
975 }
976 context
977 }
978
979 pub(crate) fn with_native_context(
980 mut self,
981 native_context: Option<crate::invocation::ScriptInvocationContext>,
982 ) -> Self {
983 self.native_context = native_context;
984 self
985 }
986
987 pub(crate) const fn with_event_target(mut self, target: Option<crate::GeometryBounds>) -> Self {
988 self.event_target = target;
989 self
990 }
991
992 pub(crate) fn with_async_scope(mut self, scope: AsyncScope) -> Self {
993 self.async_scope = Some(scope);
994 self
995 }
996
997 pub(crate) fn with_component_styles(
998 mut self,
999 global_part_styles: BTreeMap<String, crate::Style>,
1000 style: Option<crate::Style>,
1001 part_styles: BTreeMap<String, crate::Style>,
1002 ) -> Self {
1003 self.global_component_part_styles = global_part_styles;
1004 self.component_style = style;
1005 self.component_part_styles = part_styles;
1006 self
1007 }
1008
1009 pub(crate) fn reset_non_reusable_render_reads(&self) {
1010 self.non_reusable_render_reads.borrow_mut().clear();
1011 }
1012
1013 pub(crate) fn component_render_is_reusable(&self) -> bool {
1014 !self
1015 .non_reusable_render_reads
1016 .borrow()
1017 .contains(&self.component)
1018 }
1019
1020 fn mark_non_reusable_render_read(&self) {
1021 if self.phase == ExecutionPhase::Render {
1022 self.non_reusable_render_reads
1023 .borrow_mut()
1024 .insert(self.component.clone());
1025 }
1026 }
1027
1028 fn resolve_component_style(&self, part: &str, mut base: crate::Style) -> crate::Style {
1029 if let Some(style) = self.global_component_part_styles.get(part) {
1030 base = base.merged(style);
1031 }
1032 if part == "root"
1033 && let Some(style) = &self.component_style
1034 {
1035 base = base.merged(style);
1036 }
1037 if let Some(style) = self.component_part_styles.get(part) {
1038 base = base.merged(style);
1039 }
1040 base
1041 }
1042
1043 fn scoped_callback(&self, function: FnPtr) -> Result<ScriptCallback, UiContextError> {
1044 let mut callback = ScriptCallback::try_from_fn_ptr(function, self.generation)?;
1045 callback.bind_component_scope_if_unset(
1046 &self.component,
1047 self.incarnation,
1048 self.events.clone(),
1049 );
1050 if let Some(context) = &self.native_context {
1051 callback.bind_native_context_if_unset(context.clone());
1052 }
1053 Ok(callback)
1054 }
1055
1056 #[must_use]
1057 pub fn runtime(&self) -> &Rc<RefCell<UiRuntimeState>> {
1058 &self.runtime
1059 }
1060
1061 pub(crate) fn component_path(&self) -> &ComponentInstancePath {
1062 &self.component
1063 }
1064
1065 pub(crate) const fn component_incarnation(&self) -> ComponentIncarnation {
1066 self.incarnation
1067 }
1068
1069 pub(crate) fn event_schemas(&self) -> &BTreeMap<String, EventSchema> {
1070 &self.events
1071 }
1072
1073 pub(crate) fn native_context(&self) -> Option<&crate::invocation::ScriptInvocationContext> {
1074 self.native_context.as_ref()
1075 }
1076
1077 pub fn get_state(&self, field: &str) -> Result<UiValue, UiContextError> {
1084 self.runtime
1085 .try_borrow()
1086 .map_err(|_| UiContextError::Borrowed)?
1087 .component_state
1088 .get(&self.component, field)
1089 .cloned()
1090 .ok_or_else(|| UiContextError::UnknownState {
1091 component: self.component.clone(),
1092 field: field.to_owned(),
1093 })
1094 }
1095
1096 pub fn get_state_path(
1106 &self,
1107 field: &str,
1108 path: &UiValuePath,
1109 ) -> Result<UiValue, UiContextError> {
1110 Ok(self.get_state(field)?.get_path(path)?.clone())
1111 }
1112
1113 pub fn set_state(&self, field: &str, value: Dynamic) -> Result<(), UiContextError> {
1120 self.require_mutation()?;
1121 let value = UiValue::from_dynamic(value)?;
1122 let mut runtime = self
1123 .runtime
1124 .try_borrow_mut()
1125 .map_err(|_| UiContextError::Borrowed)?;
1126 let sensitive = runtime.component_state.is_sensitive(&self.component, field);
1127 let changed = runtime
1128 .component_state
1129 .set(&self.component, field, value.clone())?;
1130 if changed {
1131 runtime.dirty.insert(self.component.clone());
1132 }
1133 runtime.traces.push(
1134 crate::RuntimeTraceKind::State,
1135 self.component.to_string(),
1136 format!("set {field}"),
1137 Some(value),
1138 sensitive,
1139 );
1140 Ok(())
1141 }
1142
1143 pub fn set_state_path(
1150 &self,
1151 field: &str,
1152 path: &UiValuePath,
1153 value: Dynamic,
1154 ) -> Result<(), UiContextError> {
1155 self.require_mutation()?;
1156 let value = UiValue::from_dynamic(value)?;
1157 let mut runtime = self
1158 .runtime
1159 .try_borrow_mut()
1160 .map_err(|_| UiContextError::Borrowed)?;
1161 let mut root = runtime
1162 .component_state
1163 .get(&self.component, field)
1164 .cloned()
1165 .ok_or_else(|| UiContextError::UnknownState {
1166 component: self.component.clone(),
1167 field: field.to_owned(),
1168 })?;
1169 root.set_path(path, value.clone())?;
1170 let sensitive = runtime.component_state.is_sensitive(&self.component, field);
1171 if runtime.component_state.set(&self.component, field, root)? {
1172 runtime.dirty.insert(self.component.clone());
1173 }
1174 runtime.traces.push(
1175 crate::RuntimeTraceKind::State,
1176 self.component.to_string(),
1177 format!("set nested {field}"),
1178 Some(value),
1179 sensitive,
1180 );
1181 Ok(())
1182 }
1183
1184 pub fn get_signal(&self, signal: &crate::NativeSignal) -> Result<Dynamic, UiContextError> {
1190 self.mark_non_reusable_render_read();
1191 Ok(self
1192 .runtime
1193 .try_borrow()
1194 .map_err(|_| UiContextError::Borrowed)?
1195 .signals
1196 .read(signal)?
1197 .into_dynamic())
1198 }
1199
1200 pub fn set_signal(
1207 &self,
1208 signal: &crate::NativeSignal,
1209 value: Dynamic,
1210 ) -> Result<(), UiContextError> {
1211 self.require_mutation()?;
1212 let value = crate::SignalValue::from_dynamic_for_kind(value, signal.id().kind())?;
1213 let mut runtime = self
1214 .runtime
1215 .try_borrow_mut()
1216 .map_err(|_| UiContextError::Borrowed)?;
1217 let changed =
1218 runtime
1219 .signals
1220 .write_from(signal, value.clone(), crate::SignalWriter::Script)?;
1221 runtime.traces.push(
1222 crate::RuntimeTraceKind::Signal,
1223 signal.id().component().to_string(),
1224 format!(
1225 "set {} ({}){}",
1226 signal.id().key(),
1227 signal.id().kind().as_str(),
1228 if changed { "" } else { " unchanged" }
1229 ),
1230 None,
1231 false,
1232 );
1233 Ok(())
1234 }
1235
1236 pub fn get_signal_by_key(&self, key: &str) -> Result<Dynamic, UiContextError> {
1242 self.mark_non_reusable_render_read();
1243 let runtime = self
1244 .runtime
1245 .try_borrow()
1246 .map_err(|_| UiContextError::Borrowed)?;
1247 let signal = runtime.signals.resolve(&self.component, key)?;
1248 Ok(runtime.signals.read(&signal)?.into_dynamic())
1249 }
1250
1251 pub(crate) fn parent_optional_float_signal_ref(
1260 &self,
1261 key: &str,
1262 ) -> Result<crate::NativeSignal, UiContextError> {
1263 let runtime = self
1264 .runtime
1265 .try_borrow()
1266 .map_err(|_| UiContextError::Borrowed)?;
1267 let mut scope = self.component.parent();
1268 while let Some(component) = scope {
1269 if let Ok(signal) = runtime.signals.resolve(&component, key) {
1270 if signal.id().kind() == crate::SignalKind::OptionalFloat {
1271 return Ok(signal);
1272 }
1273 return Err(crate::SignalError::TypeMismatch {
1274 expected: crate::SignalKind::OptionalFloat,
1275 actual: signal.id().kind(),
1276 }
1277 .into());
1278 }
1279 if let Some(incarnation) = runtime.component_incarnation(&component) {
1280 let id = crate::SignalId::new_scoped(
1281 component,
1282 incarnation,
1283 key,
1284 crate::SignalKind::OptionalFloat,
1285 )?;
1286 return Ok(crate::NativeSignal::new(id));
1287 }
1288 scope = component.parent();
1289 }
1290 Err(crate::SignalError::UnknownKey {
1291 component: self.component.clone(),
1292 key: key.to_owned(),
1293 }
1294 .into())
1295 }
1296
1297 pub fn set_signal_by_key(&self, key: &str, value: Dynamic) -> Result<(), UiContextError> {
1303 self.require_mutation()?;
1304 let signal = self
1305 .runtime
1306 .try_borrow()
1307 .map_err(|_| UiContextError::Borrowed)?
1308 .signals
1309 .resolve(&self.component, key)?;
1310 self.set_signal(&signal, value)
1311 }
1312
1313 pub fn element_bounds(&self, reference: &crate::ElementRef) -> Result<UiValue, UiContextError> {
1322 let mut runtime = self
1323 .runtime
1324 .try_borrow_mut()
1325 .map_err(|_| UiContextError::Borrowed)?;
1326 let node = if self.phase == ExecutionPhase::Render {
1327 runtime
1328 .element_refs
1329 .resolve_and_track_geometry(reference, &self.component)
1330 } else {
1331 Some(runtime.element_refs.resolve(reference)?)
1332 };
1333 let Some(node) = node else {
1334 return Ok(UiValue::Null);
1335 };
1336 let geometry_registry =
1337 runtime.geometry_for(self.view.as_deref().or(self.window.as_deref()));
1338 drop(runtime);
1339 let Some(geometry) = geometry_registry.read_tracked(node, &self.component) else {
1340 return Ok(UiValue::Null);
1341 };
1342 Ok(UiValue::Map(BTreeMap::from([
1343 ("layout".to_owned(), geometry_bounds_value(geometry.layout)),
1344 ("visual".to_owned(), geometry_bounds_value(geometry.visual)),
1345 (
1346 "clip".to_owned(),
1347 geometry.clip.map_or(UiValue::Null, geometry_bounds_value),
1348 ),
1349 ])))
1350 }
1351
1352 pub fn element_bounds_by_key(&self, key: &str) -> Result<UiValue, UiContextError> {
1361 let reference = self
1362 .runtime
1363 .try_borrow()
1364 .map_err(|_| UiContextError::Borrowed)?
1365 .element_refs
1366 .resolve_key(&self.component, key)?;
1367 self.element_bounds(&reference)
1368 }
1369
1370 pub fn event_target_bounds(&self) -> Result<UiValue, UiContextError> {
1381 if self.phase != ExecutionPhase::Event {
1382 return Err(UiContextError::EventTargetOutsideEvent);
1383 }
1384 Ok(self
1385 .event_target
1386 .map_or(UiValue::Null, crate::GeometryBounds::into_value))
1387 }
1388
1389 pub fn focus_element(&self, reference: &crate::ElementRef) -> Result<(), UiContextError> {
1395 self.require_mutation()?;
1396 let window = self.window.clone().ok_or(UiContextError::MissingWindow)?;
1397 let mut runtime = self
1398 .runtime
1399 .try_borrow_mut()
1400 .map_err(|_| UiContextError::Borrowed)?;
1401 let node = runtime.element_refs.resolve(reference)?;
1402 runtime
1403 .pending_element_commands
1404 .push(crate::element_ref::ElementCommand::Focus { window, node });
1405 Ok(())
1406 }
1407
1408 pub fn focus_element_by_key(&self, key: &str) -> Result<(), UiContextError> {
1414 let reference = self
1415 .runtime
1416 .try_borrow()
1417 .map_err(|_| UiContextError::Borrowed)?
1418 .element_refs
1419 .resolve_key(&self.component, key)?;
1420 self.focus_element(&reference)
1421 }
1422
1423 pub fn scroll_element_to(
1430 &self,
1431 reference: &crate::ElementRef,
1432 x: f64,
1433 y: f64,
1434 ) -> Result<(), UiContextError> {
1435 self.require_mutation()?;
1436 if !x.is_finite() || !y.is_finite() || x < 0.0 || y < 0.0 {
1437 return Err(UiContextError::InvalidScrollOffset { x, y });
1438 }
1439 let window = self.window.clone().ok_or(UiContextError::MissingWindow)?;
1440 let mut runtime = self
1441 .runtime
1442 .try_borrow_mut()
1443 .map_err(|_| UiContextError::Borrowed)?;
1444 let node = runtime.element_refs.resolve(reference)?;
1445 runtime
1446 .pending_element_commands
1447 .push(crate::element_ref::ElementCommand::ScrollTo { window, node, x, y });
1448 Ok(())
1449 }
1450
1451 pub fn scroll_element_to_by_key(
1457 &self,
1458 key: &str,
1459 x: f64,
1460 y: f64,
1461 ) -> Result<(), UiContextError> {
1462 let reference = self
1463 .runtime
1464 .try_borrow()
1465 .map_err(|_| UiContextError::Borrowed)?
1466 .element_refs
1467 .resolve_key(&self.component, key)?;
1468 self.scroll_element_to(&reference, x, y)
1469 }
1470
1471 pub fn scroll_element_into_view(
1477 &self,
1478 reference: &crate::ElementRef,
1479 ) -> Result<(), UiContextError> {
1480 self.require_mutation()?;
1481 let window = self.window.clone().ok_or(UiContextError::MissingWindow)?;
1482 let mut runtime = self
1483 .runtime
1484 .try_borrow_mut()
1485 .map_err(|_| UiContextError::Borrowed)?;
1486 let node = runtime.element_refs.resolve(reference)?;
1487 runtime
1488 .pending_element_commands
1489 .push(crate::element_ref::ElementCommand::ScrollIntoView { window, node });
1490 Ok(())
1491 }
1492
1493 pub fn scroll_element_into_view_by_key(&self, key: &str) -> Result<(), UiContextError> {
1499 let reference = self
1500 .runtime
1501 .try_borrow()
1502 .map_err(|_| UiContextError::Borrowed)?
1503 .element_refs
1504 .resolve_key(&self.component, key)?;
1505 self.scroll_element_into_view(&reference)
1506 }
1507
1508 pub fn get_app_store(&self, store: &str, field: &str) -> Result<UiValue, UiContextError> {
1514 let mut runtime = self
1515 .runtime
1516 .try_borrow_mut()
1517 .map_err(|_| UiContextError::Borrowed)?;
1518 Ok(runtime
1519 .stores
1520 .read_tracked(&self.component, &StoreId::app(store), field)?)
1521 }
1522
1523 pub fn get_native_collection(
1532 &self,
1533 name: &str,
1534 ) -> Result<crate::NativeCollection, UiContextError> {
1535 let mut runtime = self
1536 .runtime
1537 .try_borrow_mut()
1538 .map_err(|_| UiContextError::Borrowed)?;
1539 Ok(runtime
1540 .native_collections
1541 .read_tracked(&self.component, name)?)
1542 }
1543
1544 pub fn get_native_text_document(
1550 &self,
1551 name: &str,
1552 ) -> Result<crate::NativeTextDocument, UiContextError> {
1553 let mut runtime = self
1554 .runtime
1555 .try_borrow_mut()
1556 .map_err(|_| UiContextError::Borrowed)?;
1557 Ok(runtime
1558 .native_documents
1559 .read_tracked(&self.component, name)?)
1560 }
1561
1562 pub fn get_app_store_path(
1568 &self,
1569 store: &str,
1570 field: &str,
1571 path: &UiValuePath,
1572 ) -> Result<UiValue, UiContextError> {
1573 let mut runtime = self
1574 .runtime
1575 .try_borrow_mut()
1576 .map_err(|_| UiContextError::Borrowed)?;
1577 Ok(runtime
1578 .stores
1579 .read_path_tracked(&self.component, &StoreId::app(store), field, path)?)
1580 }
1581
1582 pub fn set_app_store(
1588 &self,
1589 store: &str,
1590 field: &str,
1591 value: Dynamic,
1592 ) -> Result<(), UiContextError> {
1593 self.require_mutation()?;
1594 let value = UiValue::from_dynamic(value)?;
1595 let mut runtime = self
1596 .runtime
1597 .try_borrow_mut()
1598 .map_err(|_| UiContextError::Borrowed)?;
1599 let id = StoreId::app(store);
1600 let sensitive = runtime.stores.is_sensitive(&id, field);
1601 let invalidated = runtime.stores.write(&id, field, value.clone())?;
1602 runtime.dirty.extend(invalidated);
1603 runtime.traces.push(
1604 crate::RuntimeTraceKind::Store,
1605 format!("app:{store}"),
1606 format!("set {field}"),
1607 Some(value),
1608 sensitive,
1609 );
1610 Ok(())
1611 }
1612
1613 pub fn set_app_store_path(
1619 &self,
1620 store: &str,
1621 field: &str,
1622 path: &UiValuePath,
1623 value: Dynamic,
1624 ) -> Result<(), UiContextError> {
1625 self.require_mutation()?;
1626 let value = UiValue::from_dynamic(value)?;
1627 let mut runtime = self
1628 .runtime
1629 .try_borrow_mut()
1630 .map_err(|_| UiContextError::Borrowed)?;
1631 let id = StoreId::app(store);
1632 let sensitive = runtime.stores.is_sensitive(&id, field);
1633 let invalidated = runtime.stores.write_path(&id, field, path, value.clone())?;
1634 runtime.dirty.extend(invalidated);
1635 runtime.traces.push(
1636 crate::RuntimeTraceKind::Store,
1637 format!("app:{store}"),
1638 format!("set nested {field}"),
1639 Some(value),
1640 sensitive,
1641 );
1642 Ok(())
1643 }
1644
1645 pub fn get_window_store(&self, store: &str, field: &str) -> Result<UiValue, UiContextError> {
1652 let window = self.window.as_ref().ok_or(UiContextError::MissingWindow)?;
1653 let mut runtime = self
1654 .runtime
1655 .try_borrow_mut()
1656 .map_err(|_| UiContextError::Borrowed)?;
1657 Ok(runtime
1658 .stores
1659 .read_tracked(&self.component, &StoreId::window(window, store), field)?)
1660 }
1661
1662 pub fn get_window_store_path(
1668 &self,
1669 store: &str,
1670 field: &str,
1671 path: &UiValuePath,
1672 ) -> Result<UiValue, UiContextError> {
1673 let window = self.window.as_ref().ok_or(UiContextError::MissingWindow)?;
1674 let mut runtime = self
1675 .runtime
1676 .try_borrow_mut()
1677 .map_err(|_| UiContextError::Borrowed)?;
1678 Ok(runtime.stores.read_path_tracked(
1679 &self.component,
1680 &StoreId::window(window, store),
1681 field,
1682 path,
1683 )?)
1684 }
1685
1686 pub fn set_window_store(
1692 &self,
1693 store: &str,
1694 field: &str,
1695 value: Dynamic,
1696 ) -> Result<(), UiContextError> {
1697 self.require_mutation()?;
1698 let window = self.window.as_ref().ok_or(UiContextError::MissingWindow)?;
1699 let value = UiValue::from_dynamic(value)?;
1700 let mut runtime = self
1701 .runtime
1702 .try_borrow_mut()
1703 .map_err(|_| UiContextError::Borrowed)?;
1704 let id = StoreId::window(window, store);
1705 let sensitive = runtime.stores.is_sensitive(&id, field);
1706 let invalidated = runtime.stores.write(&id, field, value.clone())?;
1707 runtime.dirty.extend(invalidated);
1708 runtime.traces.push(
1709 crate::RuntimeTraceKind::Store,
1710 format!("window:{window}:{store}"),
1711 format!("set {field}"),
1712 Some(value),
1713 sensitive,
1714 );
1715 Ok(())
1716 }
1717
1718 pub fn set_window_store_path(
1725 &self,
1726 store: &str,
1727 field: &str,
1728 path: &UiValuePath,
1729 value: Dynamic,
1730 ) -> Result<(), UiContextError> {
1731 self.require_mutation()?;
1732 let window = self.window.as_ref().ok_or(UiContextError::MissingWindow)?;
1733 let value = UiValue::from_dynamic(value)?;
1734 let mut runtime = self
1735 .runtime
1736 .try_borrow_mut()
1737 .map_err(|_| UiContextError::Borrowed)?;
1738 let id = StoreId::window(window, store);
1739 let sensitive = runtime.stores.is_sensitive(&id, field);
1740 let invalidated = runtime.stores.write_path(&id, field, path, value.clone())?;
1741 runtime.dirty.extend(invalidated);
1742 runtime.traces.push(
1743 crate::RuntimeTraceKind::Store,
1744 format!("window:{window}:{store}"),
1745 format!("set nested {field}"),
1746 Some(value),
1747 sensitive,
1748 );
1749 Ok(())
1750 }
1751
1752 pub fn emit(&self, event: &str, payload: Dynamic) -> Result<(), UiContextError> {
1758 self.require_mutation()?;
1759 let schema = self
1760 .events
1761 .get(event)
1762 .ok_or_else(|| UiContextError::UnknownEvent(event.to_owned()))?;
1763 schema
1764 .payload
1765 .validate(&payload)
1766 .map_err(UiContextError::InvalidEvent)?;
1767 let payload = UiValue::from_dynamic(payload)?;
1768 let mut runtime = self
1769 .runtime
1770 .try_borrow_mut()
1771 .map_err(|_| UiContextError::Borrowed)?;
1772 runtime.pending_events.push(PendingEvent {
1773 target: self.component.clone(),
1774 event: UiEvent {
1775 name: event.to_owned(),
1776 payload,
1777 },
1778 });
1779 runtime.traces.push(
1780 crate::RuntimeTraceKind::Event,
1781 self.component.to_string(),
1782 format!("emit {event}"),
1783 None,
1784 true,
1785 );
1786 Ok(())
1787 }
1788
1789 pub fn dispatch_action(&self, action: &str, payload: Dynamic) -> Result<(), UiContextError> {
1795 self.require_mutation()?;
1796 let id = ActionId::parse(action)?;
1797 let payload = UiValue::from_dynamic(payload)?;
1798 let mut runtime = self
1799 .runtime
1800 .try_borrow_mut()
1801 .map_err(|_| UiContextError::Borrowed)?;
1802 let invocation = runtime.actions.dispatch(&id, payload)?;
1803 runtime.pending_actions.push(invocation);
1804 runtime.traces.push(
1805 crate::RuntimeTraceKind::Action,
1806 self.component.to_string(),
1807 format!("dispatch {action}"),
1808 None,
1809 true,
1810 );
1811 Ok(())
1812 }
1813
1814 pub fn register_action(&self, action: &str, callback: FnPtr) -> Result<(), UiContextError> {
1820 self.require_mutation()?;
1821 let id = ActionId::parse(action)?;
1822 let callback = self.scoped_callback(callback)?;
1823 self.runtime
1824 .try_borrow_mut()
1825 .map_err(|_| UiContextError::Borrowed)?
1826 .actions
1827 .register_or_replace(id, callback);
1828 Ok(())
1829 }
1830
1831 pub fn set_action_enabled(&self, action: &str, enabled: bool) -> Result<(), UiContextError> {
1837 self.require_mutation()?;
1838 let id = ActionId::parse(action)?;
1839 self.runtime
1840 .try_borrow_mut()
1841 .map_err(|_| UiContextError::Borrowed)?
1842 .actions
1843 .set_enabled(&id, enabled)?;
1844 Ok(())
1845 }
1846
1847 pub fn call_capability(
1853 &self,
1854 capability: &str,
1855 method: &str,
1856 input: Dynamic,
1857 ) -> Result<UiValue, UiContextError> {
1858 self.require_mutation()?;
1859 let id = CapabilityId::parse(capability)?;
1860 let input = UiValue::from_dynamic(input)?;
1861 let mut runtime = self
1862 .runtime
1863 .try_borrow_mut()
1864 .map_err(|_| UiContextError::Borrowed)?;
1865 let output = runtime.capabilities.call(&id, method, input)?;
1866 runtime.traces.push(
1867 crate::RuntimeTraceKind::Capability,
1868 self.component.to_string(),
1869 format!("call {capability}.{method}"),
1870 None,
1871 true,
1872 );
1873 Ok(output)
1874 }
1875
1876 fn read_locale<T>(
1877 &self,
1878 read: impl FnOnce(
1879 &LocaleManager,
1880 Option<&str>,
1881 &ComponentInstancePath,
1882 ) -> Result<T, LocaleError>,
1883 ) -> Result<T, UiContextError> {
1884 let mut runtime = self
1885 .runtime
1886 .try_borrow_mut()
1887 .map_err(|_| UiContextError::Borrowed)?;
1888 runtime
1889 .environment_dependencies
1890 .track_locale(self.window.as_deref(), &self.component);
1891 let locale = runtime
1892 .locale
1893 .as_ref()
1894 .ok_or(UiContextError::LocaleUnavailable)?;
1895 Ok(read(locale, self.window.as_deref(), &self.component)?)
1896 }
1897
1898 pub fn text(&self, key: &str) -> Result<String, UiContextError> {
1904 self.read_locale(|locale, window, component| locale.text(window, Some(component), key))
1905 }
1906
1907 pub fn text_direction(&self) -> Result<String, UiContextError> {
1913 match self
1914 .read_locale(|locale, window, component| locale.direction(window, Some(component)))?
1915 {
1916 TextDirection::LeftToRight => Ok("ltr".to_owned()),
1917 TextDirection::RightToLeft => Ok("rtl".to_owned()),
1918 }
1919 }
1920
1921 pub fn today(&self) -> Result<String, UiContextError> {
1929 Ok(self
1930 .runtime
1931 .try_borrow()
1932 .map_err(|_| UiContextError::Borrowed)?
1933 .calendar_clock
1934 .today()
1935 .to_iso())
1936 }
1937
1938 pub fn format_date(&self, iso_date: &str, style: &str) -> Result<String, UiContextError> {
1944 let style = DateStyle::parse(style)?;
1945 self.read_locale(|locale, window, component| {
1946 locale.format_date(window, Some(component), iso_date, style)
1947 })
1948 }
1949
1950 pub fn format_month_year(&self, iso_date: &str) -> Result<String, UiContextError> {
1956 self.read_locale(|locale, window, component| {
1957 locale.format_month_year(window, Some(component), iso_date)
1958 })
1959 }
1960
1961 pub fn calendar_metadata(&self) -> Result<Map, UiContextError> {
1967 let calendar = self.read_locale(|locale, window, component| {
1968 locale.calendar(window, Some(component)).cloned()
1969 })?;
1970 let dynamic = rhai::serde::to_dynamic(calendar)
1971 .map_err(|error| LocaleError::Decode(error.to_string()))?;
1972 Ok(dynamic.cast::<Map>())
1973 }
1974
1975 pub fn number_metadata(&self) -> Result<Map, UiContextError> {
1981 let number = self.read_locale(|locale, window, component| {
1982 locale.number(window, Some(component)).cloned()
1983 })?;
1984 let dynamic = rhai::serde::to_dynamic(number)
1985 .map_err(|error| LocaleError::Decode(error.to_string()))?;
1986 Ok(dynamic.cast::<Map>())
1987 }
1988
1989 pub fn format_integer(
1995 &self,
1996 value: INT,
1997 options: NumberFormatOptions,
1998 ) -> Result<String, UiContextError> {
1999 self.read_locale(|locale, window, component| {
2000 locale.format_integer(window, Some(component), value, options)
2001 })
2002 }
2003
2004 pub fn format_number(
2010 &self,
2011 value: FLOAT,
2012 options: NumberFormatOptions,
2013 ) -> Result<String, UiContextError> {
2014 self.read_locale(|locale, window, component| {
2015 locale.format_number(window, Some(component), value, options)
2016 })
2017 }
2018
2019 pub fn set_locale(&self, locale: &str) -> Result<(), UiContextError> {
2025 self.require_mutation()?;
2026 let mut runtime = self
2027 .runtime
2028 .try_borrow_mut()
2029 .map_err(|_| UiContextError::Borrowed)?;
2030 let changed = {
2031 let locales = runtime
2032 .locale
2033 .as_mut()
2034 .ok_or(UiContextError::LocaleUnavailable)?;
2035 let previous = locales.generation();
2036 locales.set_app(locale)?;
2037 locales.generation() != previous
2038 };
2039 if changed {
2040 let invalidated = runtime.environment_dependencies.invalidate_locale_app();
2041 runtime.dirty.extend(invalidated);
2042 runtime.mark_all_windows_repaint();
2043 }
2044 runtime.traces.push(
2045 crate::RuntimeTraceKind::Locale,
2046 self.component.to_string(),
2047 format!("select {locale}"),
2048 None,
2049 false,
2050 );
2051 Ok(())
2052 }
2053
2054 pub fn set_theme(&self, family: &str, variant: &str) -> Result<(), UiContextError> {
2060 self.set_theme_preference(
2061 ThemeTarget::App,
2062 ThemePreference::Fixed {
2063 selection: ThemeSelection::new(family, variant),
2064 },
2065 format!("select {family}/{variant}"),
2066 )
2067 }
2068
2069 pub fn set_window_theme(&self, family: &str, variant: &str) -> Result<(), UiContextError> {
2075 let window = self.window.clone().ok_or(UiContextError::MissingWindow)?;
2076 self.set_theme_preference(
2077 ThemeTarget::Window(window),
2078 ThemePreference::Fixed {
2079 selection: ThemeSelection::new(family, variant),
2080 },
2081 format!("select window {family}/{variant}"),
2082 )
2083 }
2084
2085 pub fn window_id(&self) -> Result<String, UiContextError> {
2091 self.window.clone().ok_or(UiContextError::MissingWindow)
2092 }
2093
2094 pub fn view_id(&self) -> Result<String, UiContextError> {
2101 self.view.clone().ok_or(UiContextError::MissingView)
2102 }
2103
2104 pub fn viewport_class(&self) -> Result<String, UiContextError> {
2110 let window = self.window.as_ref().ok_or(UiContextError::MissingWindow)?;
2111 let mut runtime = self
2112 .runtime
2113 .try_borrow_mut()
2114 .map_err(|_| UiContextError::Borrowed)?;
2115 runtime
2116 .environment_dependencies
2117 .track_viewport(window, &self.component);
2118 Ok(runtime.responsive.class(window).as_str().to_owned())
2119 }
2120
2121 pub fn open_window(&self, spec: ScriptWindowSpec) -> Result<(), UiContextError> {
2127 self.require_mutation()?;
2128 let window = self.window.as_ref().ok_or(UiContextError::MissingWindow)?;
2129 let id = spec.id.clone();
2130 let mut runtime = self
2131 .runtime
2132 .try_borrow_mut()
2133 .map_err(|_| UiContextError::Borrowed)?;
2134 runtime.windows.request_open_from(window, spec)?;
2135 runtime.traces.push(
2136 crate::RuntimeTraceKind::Window,
2137 self.component.to_string(),
2138 format!("open {id}"),
2139 None,
2140 false,
2141 );
2142 Ok(())
2143 }
2144
2145 pub fn focus_window(&self, id: &str) -> Result<(), UiContextError> {
2151 self.require_mutation()?;
2152 let window = self.window.as_ref().ok_or(UiContextError::MissingWindow)?;
2153 let mut runtime = self
2154 .runtime
2155 .try_borrow_mut()
2156 .map_err(|_| UiContextError::Borrowed)?;
2157 runtime.windows.request_focus_from(window, id)?;
2158 runtime.traces.push(
2159 crate::RuntimeTraceKind::Window,
2160 self.component.to_string(),
2161 format!("focus {id}"),
2162 None,
2163 false,
2164 );
2165 Ok(())
2166 }
2167
2168 pub fn close_window(&self, id: &str) -> Result<(), UiContextError> {
2174 self.require_mutation()?;
2175 let window = self.window.as_ref().ok_or(UiContextError::MissingWindow)?;
2176 let mut runtime = self
2177 .runtime
2178 .try_borrow_mut()
2179 .map_err(|_| UiContextError::Borrowed)?;
2180 runtime.windows.request_close_from(window, id)?;
2181 runtime.traces.push(
2182 crate::RuntimeTraceKind::Window,
2183 self.component.to_string(),
2184 format!("close {id}"),
2185 None,
2186 false,
2187 );
2188 Ok(())
2189 }
2190
2191 pub fn set_close_handler(&self, callback: FnPtr) -> Result<(), UiContextError> {
2198 self.require_mutation()?;
2199 let window = self.window.as_ref().ok_or(UiContextError::MissingWindow)?;
2200 let callback = self.scoped_callback(callback)?;
2201 self.runtime
2202 .try_borrow_mut()
2203 .map_err(|_| UiContextError::Borrowed)?
2204 .windows
2205 .set_close_handler(window, Some(callback))?;
2206 Ok(())
2207 }
2208
2209 pub fn clear_close_handler(&self) -> Result<(), UiContextError> {
2215 self.require_mutation()?;
2216 let window = self.window.as_ref().ok_or(UiContextError::MissingWindow)?;
2217 self.runtime
2218 .try_borrow_mut()
2219 .map_err(|_| UiContextError::Borrowed)?
2220 .windows
2221 .set_close_handler(window, None)?;
2222 Ok(())
2223 }
2224
2225 pub fn set_window_locale(&self, locale: &str) -> Result<(), UiContextError> {
2231 self.require_mutation()?;
2232 let window = self.window.clone().ok_or(UiContextError::MissingWindow)?;
2233 let mut runtime = self
2234 .runtime
2235 .try_borrow_mut()
2236 .map_err(|_| UiContextError::Borrowed)?;
2237 let changed = {
2238 let locales = runtime
2239 .locale
2240 .as_mut()
2241 .ok_or(UiContextError::LocaleUnavailable)?;
2242 let previous = locales.generation();
2243 locales.set_window(window.clone(), locale)?;
2244 locales.generation() != previous
2245 };
2246 if changed {
2247 let invalidated = runtime
2248 .environment_dependencies
2249 .invalidate_locale_window(&window);
2250 runtime.dirty.extend(invalidated);
2251 runtime.mark_window_repaint(window.clone());
2252 }
2253 runtime.traces.push(
2254 crate::RuntimeTraceKind::Locale,
2255 self.component.to_string(),
2256 format!("select window {window} locale {locale}"),
2257 None,
2258 false,
2259 );
2260 Ok(())
2261 }
2262
2263 pub fn set_local_locale(&self, locale: &str) -> Result<(), UiContextError> {
2269 self.require_mutation()?;
2270 let mut runtime = self
2271 .runtime
2272 .try_borrow_mut()
2273 .map_err(|_| UiContextError::Borrowed)?;
2274 let changed = {
2275 let locales = runtime
2276 .locale
2277 .as_mut()
2278 .ok_or(UiContextError::LocaleUnavailable)?;
2279 let previous = locales.generation();
2280 locales.set_scope(self.component.clone(), locale)?;
2281 locales.generation() != previous
2282 };
2283 if changed {
2284 let invalidated = runtime
2285 .environment_dependencies
2286 .invalidate_locale_scope(&self.component);
2287 runtime.dirty.extend(invalidated);
2288 if let Some(window) = &self.window {
2289 runtime.mark_window_repaint(window.clone());
2290 }
2291 }
2292 runtime.traces.push(
2293 crate::RuntimeTraceKind::Locale,
2294 self.component.to_string(),
2295 format!("select subtree locale {locale}"),
2296 None,
2297 false,
2298 );
2299 Ok(())
2300 }
2301
2302 pub fn set_local_theme(&self, family: &str, variant: &str) -> Result<(), UiContextError> {
2308 self.set_theme_preference(
2309 ThemeTarget::Local,
2310 ThemePreference::Fixed {
2311 selection: ThemeSelection::new(family, variant),
2312 },
2313 format!("select subtree {family}/{variant}"),
2314 )
2315 }
2316
2317 pub fn set_theme_system(&self, family: &str) -> Result<(), UiContextError> {
2323 self.set_theme_preference(
2324 ThemeTarget::App,
2325 ThemePreference::System {
2326 family: family.to_owned(),
2327 },
2328 format!("follow system family {family}"),
2329 )
2330 }
2331
2332 pub fn set_window_theme_system(&self, family: &str) -> Result<(), UiContextError> {
2338 let window = self.window.clone().ok_or(UiContextError::MissingWindow)?;
2339 self.set_theme_preference(
2340 ThemeTarget::Window(window),
2341 ThemePreference::System {
2342 family: family.to_owned(),
2343 },
2344 format!("follow window system family {family}"),
2345 )
2346 }
2347
2348 pub fn set_local_theme_system(&self, family: &str) -> Result<(), UiContextError> {
2354 self.set_theme_preference(
2355 ThemeTarget::Local,
2356 ThemePreference::System {
2357 family: family.to_owned(),
2358 },
2359 format!("follow subtree system family {family}"),
2360 )
2361 }
2362
2363 fn set_theme_preference(
2364 &self,
2365 target: ThemeTarget,
2366 preference: ThemePreference,
2367 message: String,
2368 ) -> Result<(), UiContextError> {
2369 self.require_mutation()?;
2370 let mut runtime = self
2371 .runtime
2372 .try_borrow_mut()
2373 .map_err(|_| UiContextError::Borrowed)?;
2374 let theme = runtime
2375 .theme
2376 .as_mut()
2377 .ok_or(UiContextError::ThemeUnavailable)?;
2378 let app_target = matches!(&target, ThemeTarget::App);
2379 match target {
2380 ThemeTarget::App => theme.set_app(preference)?,
2381 ThemeTarget::Window(window) => theme.set_window(window, preference)?,
2382 ThemeTarget::Local => theme.set_scope(self.component.clone(), preference)?,
2383 }
2384 if app_target {
2385 runtime.mark_all_windows_dirty();
2386 } else {
2387 runtime.dirty.insert(self.component.clone());
2388 }
2389 runtime.traces.push(
2390 crate::RuntimeTraceKind::Theme,
2391 self.component.to_string(),
2392 message,
2393 None,
2394 false,
2395 );
2396 Ok(())
2397 }
2398
2399 pub fn set_reduced_motion(&self, reduced: bool) -> Result<(), UiContextError> {
2406 self.require_mutation()?;
2407 let mut runtime = self
2408 .runtime
2409 .try_borrow_mut()
2410 .map_err(|_| UiContextError::Borrowed)?;
2411 runtime.animations.set_preference(if reduced {
2412 crate::MotionPreference::Reduced
2413 } else {
2414 crate::MotionPreference::Normal
2415 });
2416 let now = runtime.clock.now();
2417 runtime.animation_values = runtime.animations.snapshot(now);
2418 runtime.dirty.insert(self.component.clone());
2419 runtime.traces.push(
2420 crate::RuntimeTraceKind::State,
2421 self.component.to_string(),
2422 format!("reduced motion {reduced}"),
2423 None,
2424 false,
2425 );
2426 Ok(())
2427 }
2428
2429 pub fn load_image(&self, asset: &AssetId) -> Result<OpaqueHandle, UiContextError> {
2435 self.require_mutation()?;
2436 Ok(self
2437 .runtime
2438 .try_borrow()
2439 .map_err(|_| UiContextError::Borrowed)?
2440 .assets
2441 .load_image(asset)?
2442 .opaque()
2443 .clone())
2444 }
2445
2446 pub fn start_image_decode(
2452 &self,
2453 asset: &AssetId,
2454 success: FnPtr,
2455 error: FnPtr,
2456 ) -> Result<ImageDecodeHandle, UiContextError> {
2457 self.require_mutation()?;
2458 self.require_generation()?;
2459 let success = self.scoped_callback(success)?;
2460 let error = self.scoped_callback(error)?;
2461 let mut runtime = self
2462 .runtime
2463 .try_borrow_mut()
2464 .map_err(|_| UiContextError::Borrowed)?;
2465 crate::RuntimeBudgets::check(
2466 "image_decodes",
2467 runtime.assets.pending_decode_count().saturating_add(1),
2468 runtime.budgets.image_decodes,
2469 )?;
2470 let handle = runtime.assets.start_image_decode(
2471 asset,
2472 self.async_scope
2473 .clone()
2474 .unwrap_or_else(|| AsyncScope::Component(self.component.clone())),
2475 self.generation,
2476 success,
2477 error,
2478 )?;
2479 runtime.traces.push(
2480 crate::RuntimeTraceKind::Task,
2481 self.component.to_string(),
2482 format!("decode image {}", asset.as_str()),
2483 None,
2484 false,
2485 );
2486 Ok(handle)
2487 }
2488
2489 pub fn cancel_image_decode(&self, handle: ImageDecodeHandle) -> Result<bool, UiContextError> {
2495 self.require_mutation()?;
2496 Ok(self
2497 .runtime
2498 .try_borrow()
2499 .map_err(|_| UiContextError::Borrowed)?
2500 .assets
2501 .cancel_image_decode(handle)?)
2502 }
2503
2504 pub fn cancel_task(&self, handle: TaskHandle) -> Result<bool, UiContextError> {
2510 self.require_mutation()?;
2511 Ok(self
2512 .runtime
2513 .try_borrow_mut()
2514 .map_err(|_| UiContextError::Borrowed)?
2515 .tasks
2516 .cancel(handle))
2517 }
2518
2519 pub fn cancel_subscription(&self, handle: SubscriptionHandle) -> Result<bool, UiContextError> {
2525 self.require_mutation()?;
2526 Ok(self
2527 .runtime
2528 .try_borrow_mut()
2529 .map_err(|_| UiContextError::Borrowed)?
2530 .subscriptions
2531 .cancel(handle))
2532 }
2533
2534 pub fn pause_timeout(&self, key: &str) -> Result<bool, UiContextError> {
2540 self.require_mutation()?;
2541 let id = crate::TimerId::new(self.component.clone(), key)?;
2542 let mut runtime = self
2543 .runtime
2544 .try_borrow_mut()
2545 .map_err(|_| UiContextError::Borrowed)?;
2546 let now = runtime.clock.now();
2547 Ok(runtime.timers.pause(&id, now))
2548 }
2549
2550 pub fn resume_timeout(&self, key: &str) -> Result<bool, UiContextError> {
2556 self.require_mutation()?;
2557 let id = crate::TimerId::new(self.component.clone(), key)?;
2558 let mut runtime = self
2559 .runtime
2560 .try_borrow_mut()
2561 .map_err(|_| UiContextError::Borrowed)?;
2562 let now = runtime.clock.now();
2563 Ok(runtime.timers.resume(&id, now))
2564 }
2565
2566 pub fn cancel_timeout(&self, key: &str) -> Result<bool, UiContextError> {
2572 self.require_mutation()?;
2573 let id = crate::TimerId::new(self.component.clone(), key)?;
2574 Ok(self
2575 .runtime
2576 .try_borrow_mut()
2577 .map_err(|_| UiContextError::Borrowed)?
2578 .timers
2579 .cancel(&id))
2580 }
2581
2582 pub fn start_task(
2588 &self,
2589 capability: &str,
2590 method: &str,
2591 input: Dynamic,
2592 success: FnPtr,
2593 error: FnPtr,
2594 ) -> Result<TaskHandle, UiContextError> {
2595 self.require_mutation()?;
2596 self.require_async_start()?;
2597 self.require_generation()?;
2598 let id = CapabilityId::parse(capability)?;
2599 let input = UiValue::from_dynamic(input)?;
2600 let success = self.scoped_callback(success)?;
2601 let error = self.scoped_callback(error)?;
2602 let mut runtime = self
2603 .runtime
2604 .try_borrow_mut()
2605 .map_err(|_| UiContextError::Borrowed)?;
2606 crate::RuntimeBudgets::check(
2607 "background_tasks",
2608 runtime.tasks.active_count().saturating_add(1),
2609 runtime.budgets.background_tasks,
2610 )?;
2611 let (work, output) = runtime.capabilities.start_task(&id, method, input)?;
2612 let handle = runtime.tasks.spawn_cancellable(
2613 self.async_scope
2614 .clone()
2615 .unwrap_or_else(|| AsyncScope::Component(self.component.clone())),
2616 self.generation,
2617 success,
2618 error,
2619 output,
2620 move |cancellation| work.run(cancellation),
2621 )?;
2622 runtime.traces.push(
2623 crate::RuntimeTraceKind::Task,
2624 self.component.to_string(),
2625 format!("start {capability}.{method}"),
2626 None,
2627 true,
2628 );
2629 Ok(handle)
2630 }
2631
2632 pub fn start_subscription(
2638 &self,
2639 capability: &str,
2640 method: &str,
2641 input: Dynamic,
2642 success: FnPtr,
2643 error: FnPtr,
2644 options: SubscriptionOptions,
2645 ) -> Result<SubscriptionHandle, UiContextError> {
2646 self.require_mutation()?;
2647 self.require_async_start()?;
2648 self.require_generation()?;
2649 if !matches!(self.async_scope, Some(AsyncScope::Effect { .. })) {
2650 return Err(UiContextError::SubscriptionRequiresEffect);
2651 }
2652 let id = CapabilityId::parse(capability)?;
2653 let input = UiValue::from_dynamic(input)?;
2654 let success = self.scoped_callback(success)?;
2655 let error = self.scoped_callback(error)?;
2656 let mut runtime = self
2657 .runtime
2658 .try_borrow_mut()
2659 .map_err(|_| UiContextError::Borrowed)?;
2660 crate::RuntimeBudgets::check(
2661 "subscriptions",
2662 runtime.subscriptions.active_count().saturating_add(1),
2663 runtime.budgets.subscriptions,
2664 )?;
2665 let (work, output) = runtime
2666 .capabilities
2667 .start_subscription(&id, method, input)?;
2668 let registration = SubscriptionRegistration::new(
2669 format!("{capability}.{method}"),
2670 self.async_scope
2671 .clone()
2672 .unwrap_or_else(|| AsyncScope::Component(self.component.clone())),
2673 self.generation,
2674 success,
2675 error,
2676 output,
2677 )
2678 .with_options(options);
2679 let (handle, emitter) = runtime.subscriptions.subscribe(registration);
2680 let closer = emitter.clone();
2681 if let Err(spawn_error) = std::thread::Builder::new()
2682 .name("gpui-rhai-subscription".to_owned())
2683 .spawn(move || run_subscription_work(work, emitter, &closer))
2684 {
2685 let _ = runtime
2686 .subscriptions
2687 .cancel_with_reason(handle, SubscriptionCloseReason::StartupFailed);
2688 return Err(AsyncRuntimeError::Spawn(spawn_error).into());
2689 }
2690 runtime.traces.push(
2691 crate::RuntimeTraceKind::Subscription,
2692 self.component.to_string(),
2693 format!("start {capability}.{method}"),
2694 None,
2695 true,
2696 );
2697 Ok(handle)
2698 }
2699
2700 fn require_mutation(&self) -> Result<(), UiContextError> {
2701 if self.phase.allows_mutation() {
2702 Ok(())
2703 } else {
2704 Err(UiContextError::MutationDuringRender)
2705 }
2706 }
2707
2708 fn require_async_start(&self) -> Result<(), UiContextError> {
2709 if self.phase.allows_async_start() {
2710 Ok(())
2711 } else {
2712 Err(UiContextError::AsyncDuringSuspend)
2713 }
2714 }
2715
2716 fn require_generation(&self) -> Result<(), UiContextError> {
2717 if self.generation == ScriptGeneration::default() {
2718 Err(UiContextError::MissingGeneration)
2719 } else {
2720 Ok(())
2721 }
2722 }
2723}
2724
2725fn run_subscription_work(
2726 work: crate::SubscriptionWork,
2727 emitter: crate::SubscriptionEmitter,
2728 closer: &crate::SubscriptionEmitter,
2729) {
2730 work.run(emitter);
2731 closer.close_with_reason(SubscriptionCloseReason::WorkReturned);
2732}
2733
2734impl CustomType for UiContext {
2735 fn build(mut builder: TypeBuilder<Self>) {
2736 builder
2737 .with_name("UiContext")
2738 .with_fn(
2739 "emit",
2740 |context: &mut Self, event: ImmutableString, payload: Dynamic| {
2741 context
2742 .emit(event.as_str(), payload)
2743 .map_err(|error| Box::new(context_runtime_error(&error)))
2744 },
2745 )
2746 .with_fn(
2747 "component_style",
2748 |context: &mut Self, part: ImmutableString, base: crate::Style| {
2749 context.resolve_component_style(part.as_str(), base)
2750 },
2751 )
2752 .with_fn(
2753 "call_capability",
2754 |context: &mut Self,
2755 capability: ImmutableString,
2756 method: ImmutableString,
2757 input: Dynamic| {
2758 context
2759 .call_capability(capability.as_str(), method.as_str(), input)
2760 .map(UiValue::into_dynamic)
2761 .map_err(|error| Box::new(context_runtime_error(&error)))
2762 },
2763 );
2764 register_state_store_context_methods(&mut builder);
2765 register_native_collection_context_methods(&mut builder);
2766 register_native_document_context_methods(&mut builder);
2767 register_signal_context_methods(&mut builder);
2768 register_element_ref_context_methods(&mut builder);
2769 register_async_context_methods(&mut builder);
2770 register_action_context_methods(&mut builder);
2771 register_locale_context_methods(&mut builder);
2772 register_theme_context_methods(&mut builder);
2773 register_motion_context_methods(&mut builder);
2774 register_asset_context_methods(&mut builder);
2775 register_window_context_methods(&mut builder);
2776 }
2777}
2778
2779fn register_native_collection_context_methods(builder: &mut TypeBuilder<UiContext>) {
2780 builder.with_fn(
2781 "get_native_collection",
2782 |context: &mut UiContext, name: ImmutableString| {
2783 context
2784 .get_native_collection(name.as_str())
2785 .map_err(|error| Box::new(context_runtime_error(&error)))
2786 },
2787 );
2788}
2789
2790fn register_native_document_context_methods(builder: &mut TypeBuilder<UiContext>) {
2791 builder.with_fn(
2792 "get_native_text_document",
2793 |context: &mut UiContext, name: ImmutableString| {
2794 context
2795 .get_native_text_document(name.as_str())
2796 .map_err(|error| Box::new(context_runtime_error(&error)))
2797 },
2798 );
2799}
2800
2801fn register_state_store_context_methods(builder: &mut TypeBuilder<UiContext>) {
2802 register_state_context_methods(builder);
2803 register_app_store_context_methods(builder);
2804 register_window_store_context_methods(builder);
2805}
2806
2807fn register_state_context_methods(builder: &mut TypeBuilder<UiContext>) {
2808 builder
2809 .with_fn(
2810 "get_state",
2811 |context: &mut UiContext, field: ImmutableString| {
2812 context
2813 .get_state(field.as_str())
2814 .map(UiValue::into_dynamic)
2815 .map_err(|error| Box::new(context_runtime_error(&error)))
2816 },
2817 )
2818 .with_fn(
2819 "get_state_path",
2820 |context: &mut UiContext, field: ImmutableString, path: Array| {
2821 let path = rhai_value_path(path)?;
2822 context
2823 .get_state_path(field.as_str(), &path)
2824 .map(UiValue::into_dynamic)
2825 .map_err(|error| Box::new(context_runtime_error(&error)))
2826 },
2827 )
2828 .with_fn(
2829 "set_state",
2830 |context: &mut UiContext, field: ImmutableString, value: Dynamic| {
2831 context
2832 .set_state(field.as_str(), value)
2833 .map_err(|error| Box::new(context_runtime_error(&error)))
2834 },
2835 )
2836 .with_fn(
2837 "set_state_path",
2838 |context: &mut UiContext, field: ImmutableString, path: Array, value: Dynamic| {
2839 let path = rhai_value_path(path)?;
2840 context
2841 .set_state_path(field.as_str(), &path, value)
2842 .map_err(|error| Box::new(context_runtime_error(&error)))
2843 },
2844 );
2845}
2846
2847fn register_app_store_context_methods(builder: &mut TypeBuilder<UiContext>) {
2848 builder
2849 .with_fn(
2850 "get_app_store",
2851 |context: &mut UiContext, store: ImmutableString, field: ImmutableString| {
2852 context
2853 .get_app_store(store.as_str(), field.as_str())
2854 .map(UiValue::into_dynamic)
2855 .map_err(|error| Box::new(context_runtime_error(&error)))
2856 },
2857 )
2858 .with_fn(
2859 "get_app_store_path",
2860 |context: &mut UiContext,
2861 store: ImmutableString,
2862 field: ImmutableString,
2863 path: Array| {
2864 let path = rhai_value_path(path)?;
2865 context
2866 .get_app_store_path(store.as_str(), field.as_str(), &path)
2867 .map(UiValue::into_dynamic)
2868 .map_err(|error| Box::new(context_runtime_error(&error)))
2869 },
2870 )
2871 .with_fn(
2872 "set_app_store",
2873 |context: &mut UiContext,
2874 store: ImmutableString,
2875 field: ImmutableString,
2876 value: Dynamic| {
2877 context
2878 .set_app_store(store.as_str(), field.as_str(), value)
2879 .map_err(|error| Box::new(context_runtime_error(&error)))
2880 },
2881 )
2882 .with_fn(
2883 "set_app_store_path",
2884 |context: &mut UiContext,
2885 store: ImmutableString,
2886 field: ImmutableString,
2887 path: Array,
2888 value: Dynamic| {
2889 let path = rhai_value_path(path)?;
2890 context
2891 .set_app_store_path(store.as_str(), field.as_str(), &path, value)
2892 .map_err(|error| Box::new(context_runtime_error(&error)))
2893 },
2894 );
2895}
2896
2897fn register_window_store_context_methods(builder: &mut TypeBuilder<UiContext>) {
2898 builder
2899 .with_fn(
2900 "get_window_store",
2901 |context: &mut UiContext, store: ImmutableString, field: ImmutableString| {
2902 context
2903 .get_window_store(store.as_str(), field.as_str())
2904 .map(UiValue::into_dynamic)
2905 .map_err(|error| Box::new(context_runtime_error(&error)))
2906 },
2907 )
2908 .with_fn(
2909 "get_window_store_path",
2910 |context: &mut UiContext,
2911 store: ImmutableString,
2912 field: ImmutableString,
2913 path: Array| {
2914 let path = rhai_value_path(path)?;
2915 context
2916 .get_window_store_path(store.as_str(), field.as_str(), &path)
2917 .map(UiValue::into_dynamic)
2918 .map_err(|error| Box::new(context_runtime_error(&error)))
2919 },
2920 )
2921 .with_fn(
2922 "set_window_store",
2923 |context: &mut UiContext,
2924 store: ImmutableString,
2925 field: ImmutableString,
2926 value: Dynamic| {
2927 context
2928 .set_window_store(store.as_str(), field.as_str(), value)
2929 .map_err(|error| Box::new(context_runtime_error(&error)))
2930 },
2931 )
2932 .with_fn(
2933 "set_window_store_path",
2934 |context: &mut UiContext,
2935 store: ImmutableString,
2936 field: ImmutableString,
2937 path: Array,
2938 value: Dynamic| {
2939 let path = rhai_value_path(path)?;
2940 context
2941 .set_window_store_path(store.as_str(), field.as_str(), &path, value)
2942 .map_err(|error| Box::new(context_runtime_error(&error)))
2943 },
2944 );
2945}
2946
2947fn rhai_value_path(values: Array) -> Result<UiValuePath, Box<EvalAltResult>> {
2948 if values.len() > 64 {
2949 return Err(Box::new(EvalAltResult::ErrorRuntime(
2950 "value path must have at most 64 segments".into(),
2951 Position::NONE,
2952 )));
2953 }
2954 let segments = values
2955 .into_iter()
2956 .enumerate()
2957 .map(|(index, value)| {
2958 let actual = value.type_name().to_owned();
2959 if value.is::<ImmutableString>() {
2960 return Ok(UiValuePathSegment::Key(
2961 value.cast::<ImmutableString>().to_string(),
2962 ));
2963 }
2964 if value.is::<INT>() {
2965 let index_value = value.cast::<INT>();
2966 return usize::try_from(index_value)
2967 .map(UiValuePathSegment::Index)
2968 .map_err(|_| {
2969 Box::new(EvalAltResult::ErrorRuntime(
2970 format!("value path segment {index} must be a non-negative integer")
2971 .into(),
2972 Position::NONE,
2973 ))
2974 });
2975 }
2976 if let Some(mut selector) = value.try_cast::<Map>() {
2977 let by = selector
2978 .remove("by")
2979 .and_then(Dynamic::try_cast::<ImmutableString>);
2980 let key = selector
2981 .remove("key")
2982 .and_then(Dynamic::try_cast::<ImmutableString>);
2983 if selector.is_empty()
2984 && let (Some(by), Some(key)) = (by, key)
2985 {
2986 return Ok(UiValuePathSegment::Item {
2987 key_field: by.to_string(),
2988 key: key.to_string(),
2989 });
2990 }
2991 return Err(Box::new(EvalAltResult::ErrorRuntime(
2992 format!(
2993 "value path segment {index} keyed selector must be exactly #{{ by: string, key: string }}"
2994 )
2995 .into(),
2996 Position::NONE,
2997 )));
2998 }
2999 Err(Box::new(EvalAltResult::ErrorRuntime(
3000 format!(
3001 "value path segment {index} must be a string, non-negative integer, or keyed selector map; got {actual}"
3002 )
3003 .into(),
3004 Position::NONE,
3005 )))
3006 })
3007 .collect::<Result<Vec<_>, _>>()?;
3008 UiValuePath::new(segments).map_err(|error| {
3009 Box::new(EvalAltResult::ErrorRuntime(
3010 error.to_string().into(),
3011 Position::NONE,
3012 ))
3013 })
3014}
3015
3016fn register_signal_context_methods(builder: &mut TypeBuilder<UiContext>) {
3017 builder
3018 .with_fn(
3019 "get_signal",
3020 |context: &mut UiContext, signal: crate::NativeSignal| {
3021 context
3022 .get_signal(&signal)
3023 .map_err(|error| Box::new(context_runtime_error(&error)))
3024 },
3025 )
3026 .with_fn(
3027 "get_signal",
3028 |context: &mut UiContext, key: ImmutableString| {
3029 context
3030 .get_signal_by_key(key.as_str())
3031 .map_err(|error| Box::new(context_runtime_error(&error)))
3032 },
3033 )
3034 .with_fn(
3035 "set_signal",
3036 |context: &mut UiContext, signal: crate::NativeSignal, value: Dynamic| {
3037 context
3038 .set_signal(&signal, value)
3039 .map_err(|error| Box::new(context_runtime_error(&error)))
3040 },
3041 )
3042 .with_fn(
3043 "set_signal",
3044 |context: &mut UiContext, key: ImmutableString, value: Dynamic| {
3045 context
3046 .set_signal_by_key(key.as_str(), value)
3047 .map_err(|error| Box::new(context_runtime_error(&error)))
3048 },
3049 );
3050}
3051
3052fn register_element_ref_context_methods(builder: &mut TypeBuilder<UiContext>) {
3053 builder
3054 .with_fn(
3055 "element_bounds",
3056 |context: &mut UiContext, reference: crate::ElementRef| {
3057 context
3058 .element_bounds(&reference)
3059 .map(UiValue::into_dynamic)
3060 .map_err(|error| Box::new(context_runtime_error(&error)))
3061 },
3062 )
3063 .with_fn(
3064 "element_bounds",
3065 |context: &mut UiContext, key: ImmutableString| {
3066 context
3067 .element_bounds_by_key(key.as_str())
3068 .map(UiValue::into_dynamic)
3069 .map_err(|error| Box::new(context_runtime_error(&error)))
3070 },
3071 )
3072 .with_fn("event_target_bounds", |context: &mut UiContext| {
3073 context
3074 .event_target_bounds()
3075 .map(UiValue::into_dynamic)
3076 .map_err(|error| Box::new(context_runtime_error(&error)))
3077 })
3078 .with_fn(
3079 "focus",
3080 |context: &mut UiContext, reference: crate::ElementRef| {
3081 context
3082 .focus_element(&reference)
3083 .map_err(|error| Box::new(context_runtime_error(&error)))
3084 },
3085 )
3086 .with_fn("focus", |context: &mut UiContext, key: ImmutableString| {
3087 context
3088 .focus_element_by_key(key.as_str())
3089 .map_err(|error| Box::new(context_runtime_error(&error)))
3090 });
3091}
3092
3093fn geometry_bounds_value(bounds: crate::GeometryBounds) -> UiValue {
3094 bounds.into_value()
3095}
3096
3097fn register_action_context_methods(builder: &mut TypeBuilder<UiContext>) {
3098 builder
3099 .with_fn(
3100 "dispatch_action",
3101 |context: &mut UiContext, action: ImmutableString, payload: Dynamic| {
3102 context
3103 .dispatch_action(action.as_str(), payload)
3104 .map_err(|error| Box::new(context_runtime_error(&error)))
3105 },
3106 )
3107 .with_fn(
3108 "register_action",
3109 |call: NativeCallContext<'_>,
3110 context: &mut UiContext,
3111 action: ImmutableString,
3112 callback: FnPtr| {
3113 context_at_call(context, &call)
3114 .register_action(action.as_str(), callback)
3115 .map_err(|error| Box::new(context_runtime_error(&error)))
3116 },
3117 )
3118 .with_fn(
3119 "set_action_enabled",
3120 |context: &mut UiContext, action: ImmutableString, enabled: bool| {
3121 context
3122 .set_action_enabled(action.as_str(), enabled)
3123 .map_err(|error| Box::new(context_runtime_error(&error)))
3124 },
3125 )
3126 .with_fn(
3127 "scroll_to",
3128 |context: &mut UiContext, reference: crate::ElementRef, x: FLOAT, y: FLOAT| {
3129 context
3130 .scroll_element_to(&reference, x, y)
3131 .map_err(|error| Box::new(context_runtime_error(&error)))
3132 },
3133 )
3134 .with_fn(
3135 "scroll_to",
3136 |context: &mut UiContext, key: ImmutableString, x: FLOAT, y: FLOAT| {
3137 context
3138 .scroll_element_to_by_key(key.as_str(), x, y)
3139 .map_err(|error| Box::new(context_runtime_error(&error)))
3140 },
3141 )
3142 .with_fn(
3143 "scroll_into_view",
3144 |context: &mut UiContext, reference: crate::ElementRef| {
3145 context
3146 .scroll_element_into_view(&reference)
3147 .map_err(|error| Box::new(context_runtime_error(&error)))
3148 },
3149 )
3150 .with_fn(
3151 "scroll_into_view",
3152 |context: &mut UiContext, key: ImmutableString| {
3153 context
3154 .scroll_element_into_view_by_key(key.as_str())
3155 .map_err(|error| Box::new(context_runtime_error(&error)))
3156 },
3157 );
3158}
3159
3160fn register_async_context_methods(builder: &mut TypeBuilder<UiContext>) {
3161 builder
3162 .with_fn(
3163 "start_task",
3164 |call: NativeCallContext<'_>,
3165 context: &mut UiContext,
3166 capability: ImmutableString,
3167 method: ImmutableString,
3168 input: Dynamic,
3169 success: FnPtr,
3170 error: FnPtr| {
3171 context_at_call(context, &call)
3172 .start_task(capability.as_str(), method.as_str(), input, success, error)
3173 .map_err(|error| Box::new(context_runtime_error(&error)))
3174 },
3175 )
3176 .with_fn(
3177 "start_subscription",
3178 |call: NativeCallContext<'_>,
3179 context: &mut UiContext,
3180 capability: ImmutableString,
3181 method: ImmutableString,
3182 input: Dynamic,
3183 success: FnPtr,
3184 error: FnPtr,
3185 options: Map| {
3186 let options = subscription_options(options)?;
3187 context_at_call(context, &call)
3188 .start_subscription(
3189 capability.as_str(),
3190 method.as_str(),
3191 input,
3192 success,
3193 error,
3194 options,
3195 )
3196 .map_err(|error| Box::new(context_runtime_error(&error)))
3197 },
3198 )
3199 .with_fn(
3200 "cancel_task",
3201 |context: &mut UiContext, handle: TaskHandle| {
3202 context
3203 .cancel_task(handle)
3204 .map_err(|error| Box::new(context_runtime_error(&error)))
3205 },
3206 )
3207 .with_fn(
3208 "cancel_subscription",
3209 |context: &mut UiContext, handle: SubscriptionHandle| {
3210 context
3211 .cancel_subscription(handle)
3212 .map_err(|error| Box::new(context_runtime_error(&error)))
3213 },
3214 )
3215 .with_fn(
3216 "pause_timeout",
3217 |context: &mut UiContext, key: ImmutableString| {
3218 context
3219 .pause_timeout(key.as_str())
3220 .map_err(|error| Box::new(context_runtime_error(&error)))
3221 },
3222 )
3223 .with_fn(
3224 "resume_timeout",
3225 |context: &mut UiContext, key: ImmutableString| {
3226 context
3227 .resume_timeout(key.as_str())
3228 .map_err(|error| Box::new(context_runtime_error(&error)))
3229 },
3230 )
3231 .with_fn(
3232 "cancel_timeout",
3233 |context: &mut UiContext, key: ImmutableString| {
3234 context
3235 .cancel_timeout(key.as_str())
3236 .map_err(|error| Box::new(context_runtime_error(&error)))
3237 },
3238 );
3239}
3240
3241fn subscription_options(mut options: Map) -> Result<SubscriptionOptions, Box<EvalAltResult>> {
3242 let defaults = SubscriptionOptions::default();
3243 let delivery = options.remove("delivery").map_or(
3244 Ok(defaults.delivery()),
3245 |value| -> Result<_, Box<EvalAltResult>> {
3246 let value = value.try_cast::<ImmutableString>().ok_or_else(|| {
3247 Box::new(EvalAltResult::ErrorRuntime(
3248 "subscription delivery must be `all` or `latest`".into(),
3249 Position::NONE,
3250 ))
3251 })?;
3252 match value.as_str() {
3253 "all" => Ok(SubscriptionDeliveryPolicy::All),
3254 "latest" => Ok(SubscriptionDeliveryPolicy::Latest),
3255 _ => Err(Box::new(EvalAltResult::ErrorRuntime(
3256 "subscription delivery must be `all` or `latest`".into(),
3257 Position::NONE,
3258 ))),
3259 }
3260 },
3261 )?;
3262 let capacity = options.remove("capacity").map_or(
3263 Ok(defaults.capacity()),
3264 |value| -> Result<_, Box<EvalAltResult>> {
3265 value
3266 .try_cast::<INT>()
3267 .and_then(|value| usize::try_from(value).ok())
3268 .ok_or_else(|| {
3269 Box::new(EvalAltResult::ErrorRuntime(
3270 "subscription capacity must be a positive integer".into(),
3271 Position::NONE,
3272 ))
3273 })
3274 },
3275 )?;
3276 let throttle = options.remove("throttle_ms").map_or(
3277 Ok(defaults.throttle()),
3278 |value| -> Result<_, Box<EvalAltResult>> {
3279 value
3280 .try_cast::<INT>()
3281 .and_then(|value| u64::try_from(value).ok())
3282 .map(Duration::from_millis)
3283 .ok_or_else(|| {
3284 Box::new(EvalAltResult::ErrorRuntime(
3285 "subscription throttle_ms must be a non-negative integer".into(),
3286 Position::NONE,
3287 ))
3288 })
3289 },
3290 )?;
3291 if let Some(name) = options.keys().next() {
3292 return Err(Box::new(EvalAltResult::ErrorRuntime(
3293 format!("unknown subscription option `{name}`").into(),
3294 Position::NONE,
3295 )));
3296 }
3297 SubscriptionOptions::new(delivery, capacity, throttle).map_err(|error| {
3298 Box::new(EvalAltResult::ErrorRuntime(
3299 error.to_string().into(),
3300 Position::NONE,
3301 ))
3302 })
3303}
3304
3305fn register_locale_context_methods(builder: &mut TypeBuilder<UiContext>) {
3306 builder
3307 .with_fn("t", |context: &mut UiContext, key: ImmutableString| {
3308 context
3309 .text(key.as_str())
3310 .map_err(|error| Box::new(context_runtime_error(&error)))
3311 })
3312 .with_fn(
3313 "set_locale",
3314 |context: &mut UiContext, locale: ImmutableString| {
3315 context
3316 .set_locale(locale.as_str())
3317 .map_err(|error| Box::new(context_runtime_error(&error)))
3318 },
3319 )
3320 .with_fn("text_direction", |context: &mut UiContext| {
3321 context
3322 .text_direction()
3323 .map_err(|error| Box::new(context_runtime_error(&error)))
3324 })
3325 .with_fn("today", |context: &mut UiContext| {
3326 context
3327 .today()
3328 .map_err(|error| Box::new(context_runtime_error(&error)))
3329 })
3330 .with_fn(
3331 "format_date",
3332 |context: &mut UiContext, date: ImmutableString, style: ImmutableString| {
3333 context
3334 .format_date(date.as_str(), style.as_str())
3335 .map_err(|error| Box::new(context_runtime_error(&error)))
3336 },
3337 )
3338 .with_fn(
3339 "format_month_year",
3340 |context: &mut UiContext, date: ImmutableString| {
3341 context
3342 .format_month_year(date.as_str())
3343 .map_err(|error| Box::new(context_runtime_error(&error)))
3344 },
3345 )
3346 .with_fn("calendar", |context: &mut UiContext| {
3347 context
3348 .calendar_metadata()
3349 .map_err(|error| Box::new(context_runtime_error(&error)))
3350 })
3351 .with_fn("number", |context: &mut UiContext| {
3352 context
3353 .number_metadata()
3354 .map_err(|error| Box::new(context_runtime_error(&error)))
3355 })
3356 .with_fn("format_number", |context: &mut UiContext, value: INT| {
3357 context
3358 .format_integer(value, NumberFormatOptions::default())
3359 .map_err(|error| Box::new(context_runtime_error(&error)))
3360 })
3361 .with_fn("format_number", |context: &mut UiContext, value: FLOAT| {
3362 context
3363 .format_number(value, NumberFormatOptions::default())
3364 .map_err(|error| Box::new(context_runtime_error(&error)))
3365 })
3366 .with_fn(
3367 "format_number",
3368 |context: &mut UiContext,
3369 value: INT,
3370 options: Map|
3371 -> Result<String, Box<EvalAltResult>> {
3372 let options = number_format_options(options)?;
3373 context
3374 .format_integer(value, options)
3375 .map_err(|error| Box::new(context_runtime_error(&error)))
3376 },
3377 )
3378 .with_fn(
3379 "format_number",
3380 |context: &mut UiContext,
3381 value: FLOAT,
3382 options: Map|
3383 -> Result<String, Box<EvalAltResult>> {
3384 let options = number_format_options(options)?;
3385 context
3386 .format_number(value, options)
3387 .map_err(|error| Box::new(context_runtime_error(&error)))
3388 },
3389 );
3390}
3391
3392fn number_format_options(mut options: Map) -> Result<NumberFormatOptions, Box<EvalAltResult>> {
3393 let mut parsed = NumberFormatOptions::default();
3394 if let Some(value) = options.remove("min_fraction_digits") {
3395 let value = value.try_cast::<INT>().ok_or_else(|| {
3396 Box::new(EvalAltResult::ErrorRuntime(
3397 "min_fraction_digits must be an integer".into(),
3398 Position::NONE,
3399 ))
3400 })?;
3401 parsed.min_fraction_digits = u8::try_from(value).map_err(|_| {
3402 Box::new(EvalAltResult::ErrorRuntime(
3403 "min_fraction_digits must be between 0 and 12".into(),
3404 Position::NONE,
3405 ))
3406 })?;
3407 }
3408 if let Some(value) = options.remove("max_fraction_digits") {
3409 let value = value.try_cast::<INT>().ok_or_else(|| {
3410 Box::new(EvalAltResult::ErrorRuntime(
3411 "max_fraction_digits must be an integer".into(),
3412 Position::NONE,
3413 ))
3414 })?;
3415 parsed.max_fraction_digits = u8::try_from(value).map_err(|_| {
3416 Box::new(EvalAltResult::ErrorRuntime(
3417 "max_fraction_digits must be between 0 and 12".into(),
3418 Position::NONE,
3419 ))
3420 })?;
3421 }
3422 if let Some(value) = options.remove("grouping") {
3423 parsed.grouping = value.try_cast::<bool>().ok_or_else(|| {
3424 Box::new(EvalAltResult::ErrorRuntime(
3425 "grouping must be a bool".into(),
3426 Position::NONE,
3427 ))
3428 })?;
3429 }
3430 if let Some((unknown, _)) = options.into_iter().next() {
3431 return Err(Box::new(EvalAltResult::ErrorRuntime(
3432 format!("unknown number format option `{unknown}`").into(),
3433 Position::NONE,
3434 )));
3435 }
3436 parsed.validate().map_err(|error| {
3437 Box::new(EvalAltResult::ErrorRuntime(
3438 error.to_string().into(),
3439 Position::NONE,
3440 ))
3441 })?;
3442 Ok(parsed)
3443}
3444
3445fn register_theme_context_methods(builder: &mut TypeBuilder<UiContext>) {
3446 builder
3447 .with_fn(
3448 "set_theme",
3449 |context: &mut UiContext, family: ImmutableString, variant: ImmutableString| {
3450 context
3451 .set_theme(family.as_str(), variant.as_str())
3452 .map_err(|error| Box::new(context_runtime_error(&error)))
3453 },
3454 )
3455 .with_fn(
3456 "set_window_theme",
3457 |context: &mut UiContext, family: ImmutableString, variant: ImmutableString| {
3458 context
3459 .set_window_theme(family.as_str(), variant.as_str())
3460 .map_err(|error| Box::new(context_runtime_error(&error)))
3461 },
3462 )
3463 .with_fn(
3464 "set_local_theme",
3465 |context: &mut UiContext, family: ImmutableString, variant: ImmutableString| {
3466 context
3467 .set_local_theme(family.as_str(), variant.as_str())
3468 .map_err(|error| Box::new(context_runtime_error(&error)))
3469 },
3470 )
3471 .with_fn(
3472 "set_theme_system",
3473 |context: &mut UiContext, family: ImmutableString| {
3474 context
3475 .set_theme_system(family.as_str())
3476 .map_err(|error| Box::new(context_runtime_error(&error)))
3477 },
3478 )
3479 .with_fn(
3480 "set_window_theme_system",
3481 |context: &mut UiContext, family: ImmutableString| {
3482 context
3483 .set_window_theme_system(family.as_str())
3484 .map_err(|error| Box::new(context_runtime_error(&error)))
3485 },
3486 )
3487 .with_fn(
3488 "set_local_theme_system",
3489 |context: &mut UiContext, family: ImmutableString| {
3490 context
3491 .set_local_theme_system(family.as_str())
3492 .map_err(|error| Box::new(context_runtime_error(&error)))
3493 },
3494 );
3495}
3496
3497fn register_motion_context_methods(builder: &mut TypeBuilder<UiContext>) {
3498 builder.with_fn(
3499 "set_reduced_motion",
3500 |context: &mut UiContext, reduced: bool| {
3501 context
3502 .set_reduced_motion(reduced)
3503 .map_err(|error| Box::new(context_runtime_error(&error)))
3504 },
3505 );
3506}
3507
3508fn register_asset_context_methods(builder: &mut TypeBuilder<UiContext>) {
3509 builder
3510 .with_fn("load_image", |context: &mut UiContext, asset: AssetId| {
3511 context
3512 .load_image(&asset)
3513 .map_err(|error| Box::new(context_runtime_error(&error)))
3514 })
3515 .with_fn(
3516 "start_image_decode",
3517 |call: NativeCallContext<'_>,
3518 context: &mut UiContext,
3519 asset: AssetId,
3520 success: FnPtr,
3521 error: FnPtr| {
3522 context_at_call(context, &call)
3523 .start_image_decode(&asset, success, error)
3524 .map_err(|error| Box::new(context_runtime_error(&error)))
3525 },
3526 )
3527 .with_fn(
3528 "cancel_image_decode",
3529 |context: &mut UiContext, handle: ImageDecodeHandle| {
3530 context
3531 .cancel_image_decode(handle)
3532 .map_err(|error| Box::new(context_runtime_error(&error)))
3533 },
3534 );
3535}
3536
3537fn register_window_context_methods(builder: &mut TypeBuilder<UiContext>) {
3538 builder
3539 .with_fn("window_id", |context: &mut UiContext| {
3540 context
3541 .window_id()
3542 .map_err(|error| Box::new(context_runtime_error(&error)))
3543 })
3544 .with_fn("view_id", |context: &mut UiContext| {
3545 context
3546 .view_id()
3547 .map_err(|error| Box::new(context_runtime_error(&error)))
3548 })
3549 .with_fn("viewport_class", |context: &mut UiContext| {
3550 context
3551 .viewport_class()
3552 .map_err(|error| Box::new(context_runtime_error(&error)))
3553 })
3554 .with_fn(
3555 "open_window",
3556 |context: &mut UiContext,
3557 id: ImmutableString,
3558 title: ImmutableString,
3559 width: rhai::INT,
3560 height: rhai::INT,
3561 focus: bool| {
3562 context
3563 .open_window(ScriptWindowSpec {
3564 id: id.to_string(),
3565 title: title.to_string(),
3566 width: width.to_string().parse::<f64>().unwrap_or(f64::INFINITY),
3567 height: height.to_string().parse::<f64>().unwrap_or(f64::INFINITY),
3568 focus,
3569 })
3570 .map_err(|error| Box::new(context_runtime_error(&error)))
3571 },
3572 )
3573 .with_fn(
3574 "focus_window",
3575 |context: &mut UiContext, id: ImmutableString| {
3576 context
3577 .focus_window(id.as_str())
3578 .map_err(|error| Box::new(context_runtime_error(&error)))
3579 },
3580 )
3581 .with_fn(
3582 "close_window",
3583 |context: &mut UiContext, id: ImmutableString| {
3584 context
3585 .close_window(id.as_str())
3586 .map_err(|error| Box::new(context_runtime_error(&error)))
3587 },
3588 )
3589 .with_fn(
3590 "set_close_handler",
3591 |call: NativeCallContext<'_>, context: &mut UiContext, callback: FnPtr| {
3592 context_at_call(context, &call)
3593 .set_close_handler(callback)
3594 .map_err(|error| Box::new(context_runtime_error(&error)))
3595 },
3596 )
3597 .with_fn("clear_close_handler", |context: &mut UiContext| {
3598 context
3599 .clear_close_handler()
3600 .map_err(|error| Box::new(context_runtime_error(&error)))
3601 })
3602 .with_fn(
3603 "set_window_locale",
3604 |context: &mut UiContext, locale: ImmutableString| {
3605 context
3606 .set_window_locale(locale.as_str())
3607 .map_err(|error| Box::new(context_runtime_error(&error)))
3608 },
3609 )
3610 .with_fn(
3611 "set_local_locale",
3612 |context: &mut UiContext, locale: ImmutableString| {
3613 context
3614 .set_local_locale(locale.as_str())
3615 .map_err(|error| Box::new(context_runtime_error(&error)))
3616 },
3617 );
3618}
3619
3620fn context_at_call(context: &UiContext, call: &NativeCallContext<'_>) -> UiContext {
3621 context
3622 .clone()
3623 .with_native_context(Some(crate::invocation::ScriptInvocationContext::capture(
3624 call,
3625 )))
3626}
3627
3628pub(crate) fn register_ui_context_api(engine: &mut Engine) {
3629 engine.build_type::<UiContext>();
3630}
3631
3632fn context_runtime_error(error: &UiContextError) -> EvalAltResult {
3633 EvalAltResult::ErrorRuntime(error.to_string().into(), Position::NONE)
3634}
3635
3636#[derive(Debug, Error)]
3637pub enum UiContextError {
3638 #[error("UI runtime state is already mutably borrowed by another callback")]
3639 Borrowed,
3640 #[error("state mutation and effects are forbidden during view rendering")]
3641 MutationDuringRender,
3642 #[error("new async work is forbidden while a view is suspending")]
3643 AsyncDuringSuspend,
3644 #[error("subscriptions must be started by a declarative component effect")]
3645 SubscriptionRequiresEffect,
3646 #[error("event target geometry is available only during event callbacks")]
3647 EventTargetOutsideEvent,
3648 #[error("async work requires a bound script generation")]
3649 MissingGeneration,
3650 #[error("no locale manager is configured for this application")]
3651 LocaleUnavailable,
3652 #[error("no theme manager is configured for this application")]
3653 ThemeUnavailable,
3654 #[error("component `{component}` has no state field `{field}`")]
3655 UnknownState {
3656 component: ComponentInstancePath,
3657 field: String,
3658 },
3659 #[error("event `{0}` is not declared by this component")]
3660 UnknownEvent(String),
3661 #[error("event payload is invalid: {0}")]
3662 InvalidEvent(crate::SchemaValidationError),
3663 #[error("this UI context is not associated with a window")]
3664 MissingWindow,
3665 #[error("this UI context is not associated with a mounted script view")]
3666 MissingView,
3667 #[error(transparent)]
3668 Window(#[from] WindowCommandError),
3669 #[error(transparent)]
3670 Responsive(#[from] ResponsiveError),
3671 #[error(transparent)]
3672 State(#[from] StateError),
3673 #[error(transparent)]
3674 Store(#[from] StoreError),
3675 #[error(transparent)]
3676 NativeCollection(#[from] crate::NativeCollectionError),
3677 #[error(transparent)]
3678 Document(#[from] crate::DocumentError),
3679 #[error(transparent)]
3680 Action(#[from] ActionError),
3681 #[error(transparent)]
3682 Capability(#[from] CapabilityError),
3683 #[error(transparent)]
3684 Async(#[from] AsyncRuntimeError),
3685 #[error(transparent)]
3686 Locale(#[from] LocaleError),
3687 #[error(transparent)]
3688 Theme(#[from] ThemeError),
3689 #[error(transparent)]
3690 Asset(#[from] AssetError),
3691 #[error(transparent)]
3692 Value(#[from] UiValueError),
3693 #[error(transparent)]
3694 ValuePath(#[from] UiValuePathError),
3695 #[error(transparent)]
3696 Signal(#[from] crate::SignalError),
3697 #[error(transparent)]
3698 ElementRef(#[from] crate::ElementRefError),
3699 #[error(transparent)]
3700 Geometry(#[from] crate::GeometryError),
3701 #[error("scroll offset must be finite and non-negative, got ({x}, {y})")]
3702 InvalidScrollOffset { x: f64, y: f64 },
3703 #[error(transparent)]
3704 Callback(#[from] crate::ScriptCallbackDefinitionError),
3705 #[error(transparent)]
3706 Timer(#[from] crate::TimerError),
3707 #[error(transparent)]
3708 Budget(#[from] crate::RuntimeBudgetError),
3709}
3710
3711#[cfg(test)]
3712mod tests {
3713 use super::*;
3714 use crate::{ComponentStateSchema, RuntimeEngine, StateField, SubscriptionWork, ValueSchema};
3715
3716 fn mounted_context(phase: ExecutionPhase) -> UiContext {
3717 let path = ComponentInstancePath::root("Counter", "counter");
3718 let schema = ComponentStateSchema::new(BTreeMap::from([
3719 (
3720 "count".to_owned(),
3721 StateField::new(ValueSchema::integer(), UiValue::Integer(0)),
3722 ),
3723 (
3724 "profile".to_owned(),
3725 StateField::new(
3726 ValueSchema::UiValue,
3727 UiValue::Map(BTreeMap::from([(
3728 "name".to_owned(),
3729 UiValue::String("Ada".to_owned()),
3730 )])),
3731 ),
3732 ),
3733 ]))
3734 .unwrap();
3735 let mut state = UiRuntimeState::new();
3736 let mut render = state.component_state.begin_render();
3737 render.mount(path.clone(), &schema).unwrap();
3738 state.component_state.commit_render(render);
3739 UiContext::new(
3740 Rc::new(RefCell::new(state)),
3741 path,
3742 Some("main".to_owned()),
3743 phase,
3744 BTreeMap::from([(
3745 "change".to_owned(),
3746 EventSchema {
3747 payload: ValueSchema::integer(),
3748 },
3749 )]),
3750 )
3751 }
3752
3753 #[test]
3754 fn render_signal_reads_disable_component_reuse() {
3755 let context = mounted_context(ExecutionPhase::Render);
3756 let signal = crate::NativeSignal::new(
3757 crate::SignalId::new(
3758 context.component_path().clone(),
3759 "hot",
3760 crate::SignalKind::Float,
3761 )
3762 .unwrap(),
3763 );
3764 assert!(context.get_signal(&signal).is_err());
3765 assert!(!context.component_render_is_reusable());
3766 }
3767
3768 #[test]
3769 fn render_context_rejects_mutation() {
3770 let context = mounted_context(ExecutionPhase::Render);
3771 assert!(matches!(
3772 context.set_state("count", Dynamic::from(1_i64)),
3773 Err(UiContextError::MutationDuringRender)
3774 ));
3775 }
3776
3777 #[test]
3778 fn event_target_bounds_are_untracked_and_event_only() {
3779 let context = mounted_context(ExecutionPhase::Event).with_event_target(Some(
3780 crate::GeometryBounds::new(13.0, 24.0, 90.0, 40.0).unwrap(),
3781 ));
3782 assert_eq!(
3783 context.event_target_bounds().unwrap(),
3784 crate::GeometryBounds::new(13.0, 24.0, 90.0, 40.0)
3785 .unwrap()
3786 .into_value()
3787 );
3788 assert!(context.runtime().borrow().geometry.take_dirty().is_empty());
3789
3790 assert!(matches!(
3791 mounted_context(ExecutionPhase::Render).event_target_bounds(),
3792 Err(UiContextError::EventTargetOutsideEvent)
3793 ));
3794 assert_eq!(
3795 mounted_context(ExecutionPhase::Event)
3796 .event_target_bounds()
3797 .unwrap(),
3798 UiValue::Null
3799 );
3800 }
3801
3802 #[test]
3803 fn viewport_class_is_window_scoped_and_script_readable() {
3804 let context = mounted_context(ExecutionPhase::Render);
3805 context
3806 .runtime()
3807 .borrow_mut()
3808 .responsive
3809 .update_window("main", 480.0)
3810 .unwrap();
3811 assert_eq!(context.viewport_class().unwrap(), "compact");
3812 }
3813
3814 #[test]
3815 fn locale_and_viewport_reads_register_exact_component_dependencies() {
3816 let engine = Engine::new();
3817 let en = crate::load_locale_source(
3818 &engine,
3819 "en.rhai",
3820 include_str!("../../../registry/locales/en.rhai"),
3821 )
3822 .unwrap();
3823 let zh = crate::load_locale_source(
3824 &engine,
3825 "zh_cn.rhai",
3826 include_str!("../../../registry/locales/zh_cn.rhai"),
3827 )
3828 .unwrap();
3829 let mut state = UiRuntimeState::new();
3830 state.locale = Some(LocaleManager::new([en, zh], "en", "en").unwrap());
3831 state.responsive.update_window("main", 480.0).unwrap();
3832 state.windows.register_open("main").unwrap();
3833 let runtime = Rc::new(RefCell::new(state));
3834 let reader = ComponentInstancePath::root("View", "main").child("Reader", "reader");
3835 let unrelated = ComponentInstancePath::root("View", "main").child("Static", "static");
3836 let render_context = UiContext::new(
3837 Rc::clone(&runtime),
3838 reader.clone(),
3839 Some("main".to_owned()),
3840 ExecutionPhase::Render,
3841 BTreeMap::new(),
3842 );
3843 assert!(!render_context.text("common.loading").unwrap().is_empty());
3844 assert_eq!(render_context.viewport_class().unwrap(), "compact");
3845 let event = UiContext::new(
3846 Rc::clone(&runtime),
3847 unrelated,
3848 Some("main".to_owned()),
3849 ExecutionPhase::Event,
3850 BTreeMap::new(),
3851 );
3852 event.set_locale("zh-CN").unwrap();
3853
3854 assert_eq!(
3855 runtime.borrow_mut().drain_batch().dirty,
3856 BTreeSet::from([reader.clone()])
3857 );
3858 assert_eq!(
3859 runtime
3860 .borrow()
3861 .environment_dependencies
3862 .invalidate_viewport("main"),
3863 BTreeSet::from([reader])
3864 );
3865 assert!(runtime.borrow_mut().take_window_repaint("main"));
3866 }
3867
3868 #[test]
3869 fn event_mutations_are_batched() {
3870 let context = mounted_context(ExecutionPhase::Event);
3871 context.set_state("count", Dynamic::from(1_i64)).unwrap();
3872 context.set_state("count", Dynamic::from(2_i64)).unwrap();
3873 context.emit("change", Dynamic::from(2_i64)).unwrap();
3874
3875 let mut runtime = context.runtime().borrow_mut();
3876 let batch = runtime.drain_batch();
3877 assert_eq!(batch.dirty.len(), 1);
3878 assert_eq!(batch.events.len(), 1);
3879 assert_eq!(
3880 runtime
3881 .component_state
3882 .get(&ComponentInstancePath::root("Counter", "counter"), "count",),
3883 Some(&UiValue::Integer(2))
3884 );
3885 }
3886
3887 #[test]
3888 fn semantic_event_and_action_traces_never_retain_payload_values() {
3889 let secret = "AUDIT_ONLY_SYNTHETIC_TOKEN";
3890 let path = ComponentInstancePath::root("SecretInput", "audit");
3891 let schema = ComponentStateSchema::new(BTreeMap::from([(
3892 "token".to_owned(),
3893 StateField::new(ValueSchema::string(), UiValue::String(secret.to_owned()))
3894 .sensitive(true),
3895 )]))
3896 .unwrap();
3897 let mut state = UiRuntimeState::new();
3898 let mut render = state.component_state.begin_render();
3899 render.mount(path.clone(), &schema).unwrap();
3900 state.component_state.commit_render(render);
3901
3902 let mut engine = RuntimeEngine::new();
3903 let compiled = engine
3904 .compile("fn view() { text(\"trace\") } fn receive(ctx, payload) {}")
3905 .unwrap();
3906 state.actions.register_or_replace(
3907 crate::ActionId::parse("audit.receive").unwrap(),
3908 engine.callback(&compiled, "receive").unwrap(),
3909 );
3910 let runtime = Rc::new(RefCell::new(state));
3911 let context = UiContext::new(
3912 Rc::clone(&runtime),
3913 path,
3914 Some("main".to_owned()),
3915 ExecutionPhase::Event,
3916 BTreeMap::from([(
3917 "change".to_owned(),
3918 EventSchema {
3919 payload: ValueSchema::string(),
3920 },
3921 )]),
3922 );
3923
3924 let event_value = context.get_state("token").unwrap().into_dynamic();
3925 context.emit("change", event_value).unwrap();
3926 let action_value = context.get_state("token").unwrap().into_dynamic();
3927 context
3928 .dispatch_action("audit.receive", action_value)
3929 .unwrap();
3930
3931 let traces = runtime.borrow().traces.snapshot();
3932 let semantic = traces
3933 .iter()
3934 .filter(|trace| {
3935 matches!(
3936 trace.kind,
3937 crate::RuntimeTraceKind::Event | crate::RuntimeTraceKind::Action
3938 )
3939 })
3940 .collect::<Vec<_>>();
3941 assert_eq!(semantic.len(), 2);
3942 assert!(
3943 semantic
3944 .iter()
3945 .all(|trace| trace.sensitive && trace.payload.is_none())
3946 );
3947 assert!(!format!("{traces:?}").contains(secret));
3948 }
3949
3950 #[test]
3951 fn scripts_receive_restricted_context_without_gpui_types() {
3952 let context = mounted_context(ExecutionPhase::Event);
3953 let mut runtime = RuntimeEngine::new();
3954 let compiled = runtime
3955 .compile(
3956 r#"
3957 fn view() { text("context") }
3958 fn increment(ctx) {
3959 ctx.set_state("count", ctx.get_state("count") + 1);
3960 }
3961 "#,
3962 )
3963 .unwrap();
3964 runtime.render(&compiled).unwrap();
3965 let callback = runtime.callback(&compiled, "increment").unwrap();
3966 let _ = runtime
3967 .invoke_callback(&compiled, &callback, (context.clone(),))
3968 .unwrap();
3969 assert_eq!(context.get_state("count").unwrap(), UiValue::Integer(1));
3970 }
3971
3972 #[test]
3973 fn scripts_use_bounded_nested_and_keyed_store_paths() {
3974 let context = mounted_context(ExecutionPhase::Event);
3975 let row = |id: &str, label: &str| {
3976 UiValue::Map(BTreeMap::from([
3977 ("id".to_owned(), UiValue::String(id.to_owned())),
3978 ("label".to_owned(), UiValue::String(label.to_owned())),
3979 ]))
3980 };
3981 context
3982 .runtime()
3983 .borrow_mut()
3984 .stores
3985 .declare(
3986 StoreId::app("model"),
3987 ComponentStateSchema::new(BTreeMap::from([(
3988 "data".to_owned(),
3989 StateField::new(
3990 ValueSchema::UiValue,
3991 UiValue::Map(BTreeMap::from([(
3992 "rows".to_owned(),
3993 UiValue::Array(vec![row("alpha", "Alpha"), row("beta", "Beta")]),
3994 )])),
3995 ),
3996 )]))
3997 .unwrap(),
3998 )
3999 .unwrap();
4000 let mut runtime = RuntimeEngine::new();
4001 let compiled = runtime
4002 .compile(
4003 r#"
4004 fn view() { text("paths") }
4005 fn update(ctx) {
4006 let name = ctx.get_state_path("profile", ["name"]);
4007 ctx.set_state_path("profile", ["name"], name + " Lovelace");
4008 let label = ctx.get_app_store_path(
4009 "model",
4010 "data",
4011 ["rows", #{ by: "id", key: "beta" }, "label"]
4012 );
4013 ctx.set_app_store_path(
4014 "model",
4015 "data",
4016 ["rows", #{ by: "id", key: "beta" }, "label"],
4017 label + "!"
4018 );
4019 }
4020 "#,
4021 )
4022 .unwrap();
4023 runtime.render(&compiled).unwrap();
4024 let callback = runtime.callback(&compiled, "update").unwrap();
4025 let _: Dynamic = runtime
4026 .invoke_callback(&compiled, &callback, (context.clone(),))
4027 .unwrap();
4028
4029 let profile = context.get_state("profile").unwrap();
4030 assert_eq!(
4031 profile
4032 .get_path(
4033 &UiValuePath::new(vec![UiValuePathSegment::Key("name".to_owned())]).unwrap()
4034 )
4035 .unwrap(),
4036 &UiValue::String("Ada Lovelace".to_owned())
4037 );
4038 let row_path = UiValuePath::new(vec![
4039 UiValuePathSegment::Key("rows".to_owned()),
4040 UiValuePathSegment::Item {
4041 key_field: "id".to_owned(),
4042 key: "beta".to_owned(),
4043 },
4044 UiValuePathSegment::Key("label".to_owned()),
4045 ])
4046 .unwrap();
4047 assert_eq!(
4048 context
4049 .runtime()
4050 .borrow_mut()
4051 .stores
4052 .read_path_tracked(
4053 &ComponentInstancePath::root("Test", "reader"),
4054 &StoreId::app("model"),
4055 "data",
4056 &row_path,
4057 )
4058 .unwrap(),
4059 UiValue::String("Beta!".to_owned())
4060 );
4061 assert!(rhai_value_path(vec![Dynamic::from(-1_i64)]).is_err());
4062 }
4063
4064 #[test]
4065 fn script_theme_switch_preserves_component_state_and_ast_generation() {
4066 let context = mounted_context(ExecutionPhase::Event);
4067 context.set_state("count", Dynamic::from(7_i64)).unwrap();
4068 let mut runtime = RuntimeEngine::new();
4069 let dark = crate::load_theme_source(
4070 runtime.engine(),
4071 "default_dark.rhai",
4072 include_str!("../../../registry/themes/default_dark.rhai"),
4073 )
4074 .unwrap();
4075 let mocha = crate::load_theme_source(
4076 runtime.engine(),
4077 "catppuccin_mocha.rhai",
4078 include_str!("../../../registry/themes/catppuccin_mocha.rhai"),
4079 )
4080 .unwrap();
4081 context.runtime().borrow_mut().theme = Some(
4082 ThemeManager::from_variants([dark, mocha], ThemeSelection::new("Default", "Dark"))
4083 .unwrap(),
4084 );
4085 let compiled = runtime
4086 .compile(
4087 r#"
4088 fn view() { text("theme") }
4089 fn switch_theme(ctx) { ctx.set_theme("Catppuccin", "Mocha"); }
4090 "#,
4091 )
4092 .unwrap();
4093 runtime.render(&compiled).unwrap();
4094 let generation = compiled.generation();
4095 let callback = runtime.callback(&compiled, "switch_theme").unwrap();
4096 let _ = runtime
4097 .invoke_callback(&compiled, &callback, (context.clone(),))
4098 .unwrap();
4099
4100 assert_eq!(context.get_state("count").unwrap(), UiValue::Integer(7));
4101 assert_eq!(compiled.generation(), generation);
4102 let state = context.runtime().borrow();
4103 let theme = state
4104 .theme
4105 .as_ref()
4106 .unwrap()
4107 .resolve(Some("main"), None, crate::SystemAppearance::Dark)
4108 .unwrap();
4109 assert_eq!(theme.variant().family, "Catppuccin");
4110 assert_eq!(theme.variant().name, "Mocha");
4111 drop(state);
4112 context.set_theme_system("Default").unwrap();
4113 let state = context.runtime().borrow();
4114 assert!(matches!(
4115 state.theme.as_ref().unwrap().app_preference(),
4116 ThemePreference::System { family } if family == "Default"
4117 ));
4118 }
4119
4120 #[test]
4121 fn script_reads_fixed_today_calendar_and_locale_formatters() {
4122 let mut engine = Engine::new();
4123 register_ui_context_api(&mut engine);
4124 let bundle = crate::load_locale_source(
4125 &engine,
4126 "en.rhai",
4127 include_str!("../../../registry/locales/en.rhai"),
4128 )
4129 .unwrap();
4130 let mut state = UiRuntimeState::new();
4131 state.locale = Some(LocaleManager::new([bundle], "en", "en").unwrap());
4132 state.calendar_clock =
4133 crate::CalendarClock::fixed(crate::GregorianDate::parse_iso("2026-08-29").unwrap());
4134 let context = UiContext::new(
4135 Rc::new(RefCell::new(state)),
4136 ComponentInstancePath::root("App", "root"),
4137 Some("main".to_owned()),
4138 ExecutionPhase::Render,
4139 BTreeMap::new(),
4140 );
4141 let mut scope = rhai::Scope::new();
4142 scope.push("ctx", context);
4143 let values = engine
4144 .eval_with_scope::<rhai::Array>(
4145 &mut scope,
4146 r#"[
4147 ctx.today(),
4148 ctx.format_date("2024-02-29", "long"),
4149 ctx.format_number(12345, #{ min_fraction_digits: 0, max_fraction_digits: 0 }),
4150 ctx.calendar().first_weekday,
4151 ]"#,
4152 )
4153 .unwrap();
4154 assert_eq!(values[0].clone_cast::<String>(), "2026-08-29");
4155 assert_eq!(
4156 values[1].clone_cast::<String>(),
4157 "Thursday, February 29, 2024"
4158 );
4159 assert_eq!(values[2].clone_cast::<String>(), "12,345");
4160 assert_eq!(values[3].clone_cast::<String>(), "sunday");
4161 }
4162
4163 #[test]
4164 fn returning_subscription_work_closes_external_emitters_with_reason() {
4165 let mut engine = RuntimeEngine::new();
4166 let compiled = engine
4167 .compile(
4168 r#"
4169 fn view() { text("subscription") }
4170 fn success(ctx, value) { value }
4171 fn failure(ctx, error) { error }
4172 "#,
4173 )
4174 .unwrap();
4175 let generation = compiled.generation();
4176 let success = engine.callback(&compiled, "success").unwrap();
4177 let failure = engine.callback(&compiled, "failure").unwrap();
4178 let mut state = UiRuntimeState::new();
4179 let registration = SubscriptionRegistration::new(
4180 "app.stream.watch",
4181 AsyncScope::App,
4182 generation,
4183 success,
4184 failure,
4185 ValueSchema::integer(),
4186 );
4187 let (_, emitter) = state.subscriptions.subscribe(registration);
4188 let external = emitter.clone();
4189 run_subscription_work(SubscriptionWork::new(|_| {}), emitter.clone(), &emitter);
4190
4191 assert!(matches!(
4192 external.emit(UiValue::Integer(1)),
4193 Err(AsyncRuntimeError::Closed {
4194 reason: SubscriptionCloseReason::WorkReturned
4195 })
4196 ));
4197 let _ = state.subscriptions.drain(generation);
4198 assert_eq!(
4199 state.subscriptions.take_closures()[0].reason,
4200 SubscriptionCloseReason::WorkReturned
4201 );
4202 let registration = SubscriptionRegistration::new(
4203 "app.stream.trace",
4204 AsyncScope::App,
4205 generation,
4206 engine.callback(&compiled, "success").unwrap(),
4207 engine.callback(&compiled, "failure").unwrap(),
4208 ValueSchema::integer(),
4209 );
4210 let (_, emitter) = state.subscriptions.subscribe(registration);
4211 emitter.close_with_reason(SubscriptionCloseReason::WorkReturned);
4212 let _ = state.subscriptions.drain(generation);
4213 state.trace_subscription_closures();
4214 let traces = state.traces.snapshot();
4215 assert!(traces.iter().any(|trace| {
4216 trace.kind == crate::RuntimeTraceKind::Subscription
4217 && trace.message == "close app.stream.trace: the subscription work returned"
4218 }));
4219 }
4220
4221 #[test]
4222 fn receiver_subscription_forwards_external_values_until_sender_drop() {
4223 let mut engine = RuntimeEngine::new();
4224 let compiled = engine
4225 .compile(
4226 r#"
4227 fn view() { text("subscription") }
4228 fn success(ctx, value) { value }
4229 fn failure(ctx, error) { error }
4230 "#,
4231 )
4232 .unwrap();
4233 let generation = compiled.generation();
4234 let registration = SubscriptionRegistration::new(
4235 "app.stream.receiver",
4236 AsyncScope::App,
4237 generation,
4238 engine.callback(&compiled, "success").unwrap(),
4239 engine.callback(&compiled, "failure").unwrap(),
4240 ValueSchema::integer(),
4241 );
4242 let mut subscriptions = SubscriptionRegistry::new();
4243 let (_, emitter) = subscriptions.subscribe(registration);
4244 let external = emitter.clone();
4245 let closer = emitter.clone();
4246 let (sender, receiver) = std::sync::mpsc::channel();
4247 let worker = std::thread::spawn(move || {
4248 run_subscription_work(SubscriptionWork::from_receiver(receiver), emitter, &closer);
4249 });
4250 sender.send(UiValue::Integer(42)).unwrap();
4251 drop(sender);
4252 worker.join().unwrap();
4253
4254 let deliveries = subscriptions.drain(generation);
4255 assert_eq!(deliveries[0].payload, UiValue::Integer(42));
4256 assert!(matches!(
4257 external.emit(UiValue::Integer(43)),
4258 Err(AsyncRuntimeError::Closed {
4259 reason: SubscriptionCloseReason::WorkReturned
4260 })
4261 ));
4262 }
4263
4264 #[test]
4265 fn receiver_subscription_waits_for_lossless_capacity_without_ending_the_stream() {
4266 let mut engine = RuntimeEngine::new();
4267 let compiled = engine
4268 .compile(
4269 "fn view() { text(\"stream\") } fn success(ctx, value) {} fn failure(ctx, value) {}",
4270 )
4271 .unwrap();
4272 engine.render(&compiled).unwrap();
4273 let generation = compiled.generation();
4274 let registration = SubscriptionRegistration::new(
4275 "stream",
4276 AsyncScope::App,
4277 generation,
4278 engine.callback(&compiled, "success").unwrap(),
4279 engine.callback(&compiled, "failure").unwrap(),
4280 ValueSchema::integer(),
4281 )
4282 .with_capacity(1)
4283 .unwrap();
4284 let mut subscriptions = SubscriptionRegistry::new();
4285 let (_, emitter) = subscriptions.subscribe(registration);
4286 let closer = emitter.clone();
4287 let (sender, receiver) = std::sync::mpsc::channel();
4288 let worker = std::thread::spawn(move || {
4289 run_subscription_work(SubscriptionWork::from_receiver(receiver), emitter, &closer);
4290 });
4291 sender.send(UiValue::Integer(1)).unwrap();
4292 sender.send(UiValue::Integer(2)).unwrap();
4293 let deadline = std::time::Instant::now() + Duration::from_secs(1);
4294 let first = loop {
4295 let values = subscriptions.drain(generation);
4296 if !values.is_empty() {
4297 break values;
4298 }
4299 assert!(std::time::Instant::now() < deadline);
4300 std::thread::yield_now();
4301 };
4302 assert_eq!(first[0].payload, UiValue::Integer(1));
4303 let second = loop {
4304 let values = subscriptions.drain(generation);
4305 if !values.is_empty() {
4306 break values;
4307 }
4308 assert!(std::time::Instant::now() < deadline);
4309 std::thread::yield_now();
4310 };
4311 assert_eq!(second[0].payload, UiValue::Integer(2));
4312 assert_eq!(subscriptions.active_count(), 1);
4313 drop(sender);
4314 worker.join().unwrap();
4315 }
4316
4317 #[test]
4318 fn runtime_transaction_rollback_restores_cancelled_subscription() {
4319 let mut engine = RuntimeEngine::new();
4320 let compiled = engine
4321 .compile(
4322 "fn view() { text(\"test\") } fn success(ctx, value) {} fn failure(ctx, value) {}",
4323 )
4324 .unwrap();
4325 engine.render(&compiled).unwrap();
4326 let success = engine.callback(&compiled, "success").unwrap();
4327 let error = engine.callback(&compiled, "failure").unwrap();
4328 let generation = compiled.generation();
4329 let mut state = UiRuntimeState::new();
4330 let (handle, emitter) = state.subscriptions.subscribe(SubscriptionRegistration::new(
4331 "rollback",
4332 AsyncScope::App,
4333 generation,
4334 success,
4335 error,
4336 ValueSchema::integer(),
4337 ));
4338 let checkpoint = state.begin_transaction().unwrap();
4339 assert!(state.subscriptions.cancel(handle));
4340 assert_eq!(emitter.close_reason(), None);
4341 state.rollback_transaction(checkpoint).unwrap();
4342 assert_eq!(state.subscriptions.active_count(), 1);
4343 emitter.emit(UiValue::Integer(7)).unwrap();
4344 assert_eq!(state.subscriptions.drain(generation).len(), 1);
4345 }
4346
4347 #[test]
4348 fn transaction_tokens_cannot_cross_runtime_or_commit_unbalanced() {
4349 let mut first = UiRuntimeState::new();
4350 let mut second = UiRuntimeState::new();
4351 let checkpoint = first.begin_transaction().unwrap();
4352 assert!(matches!(
4353 second.rollback_transaction(checkpoint.clone()),
4354 Err(UiTransactionError::ForeignSnapshot)
4355 ));
4356 assert!(matches!(
4357 second.commit_transaction(),
4358 Err(UiTransactionError::Unbalanced { .. })
4359 ));
4360 first.rollback_transaction(checkpoint).unwrap();
4361 }
4362
4363 #[test]
4364 fn presentation_geometry_and_capture_are_isolated_per_view() {
4365 let mut first = crate::RetainedUiTree::new();
4366 first.reconcile(crate::UiNode::text("first")).unwrap();
4367 let mut second = crate::RetainedUiTree::new();
4368 second.reconcile(crate::UiNode::text("second")).unwrap();
4369 let first_node = first.root_id().unwrap();
4370 let second_node = second.root_id().unwrap();
4371 assert_eq!(
4372 first_node, second_node,
4373 "tree-local IDs intentionally overlap"
4374 );
4375
4376 let mut state = UiRuntimeState::new();
4377 state.ensure_presentation("first");
4378 state.ensure_presentation("second");
4379 let first_geometry = state.geometry_for(Some("first"));
4380 let second_geometry = state.geometry_for(Some("second"));
4381 let bounds = |width| crate::ElementGeometry {
4382 layout: crate::GeometryBounds::new(0.0, 0.0, width, 20.0).unwrap(),
4383 visual: crate::GeometryBounds::new(0.0, 0.0, width, 20.0).unwrap(),
4384 clip: None,
4385 };
4386 first_geometry.update(first_node, bounds(100.0));
4387 second_geometry.update(second_node, bounds(300.0));
4388 second_geometry.begin_frame();
4389 assert!(
4390 (first_geometry.get(first_node).unwrap().visual.width - 100.0).abs() < f64::EPSILON
4391 );
4392 assert!(first_geometry.is_presented(first_node));
4393
4394 let first_capture = state.pointer_capture_for(Some("first"));
4395 let second_capture = state.pointer_capture_for(Some("second"));
4396 first_capture.capture(0, first_node);
4397 second_capture.capture(0, second_node);
4398 assert_eq!(first_capture.captured(0), Some(first_node));
4399 assert_eq!(second_capture.captured(0), Some(second_node));
4400 }
4401
4402 #[test]
4403 fn focus_by_ref_key_queues_a_window_scoped_retained_command() {
4404 let context = mounted_context(ExecutionPhase::Event);
4405 let mut tree = crate::RetainedUiTree::new();
4406 tree.reconcile(crate::UiNode::text("field")).unwrap();
4407 let reference = crate::ElementRef::new(
4408 crate::ElementRefId::new(context.component_path().clone(), "field").unwrap(),
4409 );
4410 context.runtime().borrow_mut().element_refs.reconcile(
4411 context.component_path(),
4412 BTreeMap::from([(reference.id().clone(), tree.root_id().unwrap())]),
4413 );
4414 let node = tree.root_id().unwrap();
4415 context
4416 .runtime()
4417 .borrow()
4418 .geometry_for(Some("main"))
4419 .update(
4420 node,
4421 crate::ElementGeometry {
4422 layout: crate::GeometryBounds::new(1.0, 2.0, 120.0, 24.0).unwrap(),
4423 visual: crate::GeometryBounds::new(3.0, 4.0, 120.0, 24.0).unwrap(),
4424 clip: None,
4425 },
4426 );
4427 assert!(matches!(
4428 context.element_bounds_by_key("field").unwrap(),
4429 UiValue::Map(bounds)
4430 if matches!(bounds.get("visual"), Some(UiValue::Map(visual))
4431 if visual.get("width") == Some(&UiValue::Float(120.0)))
4432 ));
4433 context.focus_element_by_key("field").unwrap();
4434 context
4435 .scroll_element_to_by_key("field", 12.0, 24.0)
4436 .unwrap();
4437 context.scroll_element_into_view_by_key("field").unwrap();
4438 let commands = context
4439 .runtime()
4440 .borrow_mut()
4441 .take_window_element_commands("main");
4442 assert!(matches!(
4443 commands.as_slice(),
4444 [crate::element_ref::ElementCommand::Focus { node, .. },
4445 crate::element_ref::ElementCommand::ScrollTo { node: scroll_node, x, y, .. },
4446 crate::element_ref::ElementCommand::ScrollIntoView { node: reveal_node, .. }]
4447 if *node == tree.root_id().unwrap()
4448 && scroll_node == node
4449 && reveal_node == node
4450 && (*x - 12.0).abs() < f64::EPSILON
4451 && (*y - 24.0).abs() < f64::EPSILON
4452 ));
4453 }
4454}