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, AssetError, AssetId, AssetRegistry,
15 AsyncRuntimeError, AsyncScope, CalendarClock, CapabilityError, CapabilityId,
16 CapabilityRegistry, ComponentInstancePath, DateStyle, EventSchema, ImageDecodeHandle,
17 LocaleError, LocaleManager, MotionKey, MotionRuntime, 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
26fn domain_is_within(domain: &str, scope: &str) -> bool {
27 domain == scope
28 || domain
29 .strip_prefix(scope)
30 .is_some_and(|suffix| suffix.starts_with('/'))
31}
32
33#[derive(Clone, Copy, Debug, Eq, PartialEq)]
34pub enum ExecutionPhase {
35 Init,
36 Render,
37 Event,
38 Suspend,
39 Resume,
40 Dispose,
41}
42
43#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
46pub struct ComponentIncarnation(u64);
47
48impl ComponentIncarnation {
49 pub(crate) const fn unscoped() -> Self {
50 Self(0)
51 }
52
53 #[must_use]
54 pub const fn get(self) -> u64 {
55 self.0
56 }
57}
58
59#[derive(Clone, Copy, Debug, Eq, PartialEq)]
60struct RuntimeStateId(u64);
61
62impl Default for RuntimeStateId {
63 fn default() -> Self {
64 static NEXT_ID: AtomicU64 = AtomicU64::new(1);
65 Self(NEXT_ID.fetch_add(1, Ordering::Relaxed))
66 }
67}
68
69enum ThemeTarget {
70 App,
71 Window(String),
72 Local,
73}
74
75impl ExecutionPhase {
76 const fn allows_mutation(self) -> bool {
77 matches!(
78 self,
79 Self::Init | Self::Event | Self::Suspend | Self::Resume | Self::Dispose
80 )
81 }
82
83 const fn allows_async_start(self) -> bool {
84 matches!(self, Self::Init | Self::Event | Self::Resume)
85 }
86}
87
88#[derive(Debug, Default)]
89pub struct UiRuntimeState {
90 runtime_id: RuntimeStateId,
91 transaction_depth: usize,
92 pub component_state: StateStore,
93 pub stores: StoreRegistry,
94 pub native_collections: crate::NativeCollectionRegistry,
95 pub native_documents: crate::NativeTextDocumentRegistry,
96 #[cfg(feature = "charts")]
97 pub native_chart_data: BTreeMap<String, crate::NativeChartData>,
98 pub actions: ActionRegistry,
99 pub capabilities: CapabilityRegistry,
100 pub tasks: TaskRegistry,
101 pub subscriptions: SubscriptionRegistry,
102 pub timers: crate::TimerRegistry,
103 pub locale: Option<LocaleManager>,
104 pub calendar_clock: CalendarClock,
105 pub clock: crate::RuntimeClock,
106 pub theme: Option<ThemeManager>,
107 component_styles: crate::ComponentStyleSheet,
108 pub assets: AssetRegistry,
109 pub motions: MotionRuntime,
110 pub effects: crate::EffectRegistry,
111 pub signals: crate::SignalRegistry,
112 pub element_refs: crate::ElementRefRegistry,
113 pub(crate) geometry: crate::GeometryRegistry,
114 pub(crate) pointer_capture: crate::PointerCaptureRegistry,
115 presentations: BTreeMap<String, ViewPresentationState>,
116 pub budgets: crate::RuntimeBudgets,
117 pub virtual_requests: crate::VirtualRequestRegistry,
118 pub windows: WindowCommandRegistry,
119 pub responsive: ResponsiveRuntime,
120 pub(crate) environment_dependencies:
121 crate::environment_dependency::EnvironmentDependencyRegistry,
122 pub motion_values: BTreeMap<MotionKey, f64>,
123 pub(crate) motion_ghosts: Vec<crate::motion::MotionGhost>,
124 pub(crate) window_appearances: BTreeMap<String, crate::SystemAppearance>,
125 pub traces: crate::TraceBuffer,
126 component_event_handlers: BTreeMap<(ComponentInstancePath, String), ScriptCallback>,
127 component_incarnations: BTreeMap<ComponentInstancePath, ComponentIncarnation>,
128 next_component_incarnation: u64,
129 dirty: BTreeSet<ComponentInstancePath>,
130 pending_events: Vec<PendingEvent>,
131 pending_actions: Vec<ActionInvocation>,
132 pending_async: Vec<crate::AsyncDelivery>,
133 pending_element_commands: Vec<crate::element_ref::ElementCommand>,
134 repaint_windows: BTreeSet<String>,
135 component_style_generation: u64,
136}
137
138impl UiRuntimeState {
139 const SUSPENDED_DELIVERY_CAPACITY: usize = 256;
140 #[must_use]
141 pub fn new() -> Self {
142 Self::default()
143 }
144
145 pub(crate) fn ensure_component_incarnation(
146 &mut self,
147 component: &ComponentInstancePath,
148 ) -> ComponentIncarnation {
149 if let Some(incarnation) = self.component_incarnations.get(component) {
150 return *incarnation;
151 }
152 let next = self.next_component_incarnation.max(1);
153 let incarnation = ComponentIncarnation(next);
154 self.next_component_incarnation = next.saturating_add(1);
155 self.component_incarnations
156 .insert(component.clone(), incarnation);
157 incarnation
158 }
159
160 pub(crate) fn component_incarnation(
161 &self,
162 component: &ComponentInstancePath,
163 ) -> Option<ComponentIncarnation> {
164 self.component_incarnations.get(component).copied()
165 }
166
167 pub(crate) fn component_incarnations_snapshot(
168 &self,
169 ) -> BTreeMap<ComponentInstancePath, ComponentIncarnation> {
170 self.component_incarnations.clone()
171 }
172
173 pub(crate) fn ensure_presentation(&mut self, view: &str) {
174 self.presentations.entry(view.to_owned()).or_default();
175 }
176
177 pub fn presentation_geometry(&mut self, view: &str) -> crate::GeometryRegistry {
179 self.ensure_presentation(view);
180 self.geometry_for(Some(view))
181 }
182
183 pub fn presentation_pointer_capture(&mut self, view: &str) -> crate::PointerCaptureRegistry {
185 self.ensure_presentation(view);
186 self.pointer_capture_for(Some(view))
187 }
188
189 pub(crate) fn geometry_for(&self, view: Option<&str>) -> crate::GeometryRegistry {
190 view.and_then(|view| self.presentations.get(view))
191 .map_or_else(|| self.geometry.clone(), |state| state.geometry.clone())
192 }
193
194 pub(crate) fn pointer_capture_for(&self, view: Option<&str>) -> crate::PointerCaptureRegistry {
195 view.and_then(|view| self.presentations.get(view))
196 .map_or_else(
197 || self.pointer_capture.clone(),
198 |state| state.pointer_capture.clone(),
199 )
200 }
201
202 pub(crate) fn remove_presentation(&mut self, view: &str) {
203 self.presentations.remove(view);
204 }
205
206 pub fn set_component_state_from_host(
213 &mut self,
214 component: &ComponentInstancePath,
215 field: &str,
216 value: UiValue,
217 ) -> Result<bool, crate::StateError> {
218 let changed = self.component_state.set(component, field, value)?;
219 if changed {
220 self.dirty.insert(component.clone());
221 }
222 Ok(changed)
223 }
224
225 pub fn replace_native_collection_from_host(
231 &mut self,
232 name: &str,
233 collection: crate::NativeCollection,
234 ) -> Result<bool, crate::NativeCollectionError> {
235 let invalidated = self.native_collections.replace(name, collection)?;
236 let changed = !invalidated.is_empty();
237 self.dirty.extend(invalidated);
238 Ok(changed)
239 }
240
241 pub fn replace_native_text_document_from_host(
247 &mut self,
248 name: &str,
249 document: crate::NativeTextDocument,
250 ) -> Result<bool, crate::DocumentError> {
251 let invalidated = self.native_documents.replace(name, document)?;
252 let changed = !invalidated.is_empty();
253 self.dirty.extend(invalidated);
254 Ok(changed)
255 }
256
257 #[cfg(feature = "charts")]
263 pub fn register_native_chart_data(
264 &mut self,
265 name: impl Into<String>,
266 data: crate::NativeChartData,
267 ) -> Result<(), UiContextError> {
268 let name = name.into();
269 if name.is_empty()
270 || name.len() > 128
271 || !name
272 .chars()
273 .next()
274 .is_some_and(|character| character.is_ascii_alphabetic())
275 || !name
276 .chars()
277 .all(|character| character.is_ascii_alphanumeric() || character == '_')
278 {
279 return Err(UiContextError::InvalidChartDataName(name));
280 }
281 self.native_chart_data.insert(name, data);
282 Ok(())
283 }
284
285 pub fn replace_theme_variant_from_host(
292 &mut self,
293 variant: crate::ThemeVariant,
294 ) -> Result<(), crate::ThemeError> {
295 self.theme
296 .as_mut()
297 .ok_or(crate::ThemeError::NoVariants)?
298 .replace_variant(variant)?;
299 self.dirty.extend(self.component_state.paths());
300 Ok(())
301 }
302
303 pub fn select_theme_from_host(
309 &mut self,
310 family: &str,
311 variant: &str,
312 ) -> Result<bool, crate::ThemeError> {
313 let theme = self.theme.as_mut().ok_or(crate::ThemeError::NoVariants)?;
314 let previous = theme.generation();
315 theme.set_app(ThemePreference::Fixed {
316 selection: ThemeSelection::new(family, variant),
317 })?;
318 let changed = theme.generation() != previous;
319 if changed {
320 let invalidated = self.environment_dependencies.invalidate_theme_app();
321 self.dirty.extend(invalidated);
322 self.mark_all_windows_dirty();
323 }
324 Ok(changed)
325 }
326
327 pub fn select_locale_from_host(&mut self, locale: &str) -> Result<bool, crate::LocaleError> {
333 let locales = self
334 .locale
335 .as_mut()
336 .ok_or_else(|| crate::LocaleError::UnknownLocale(locale.to_owned()))?;
337 let previous = locales.generation();
338 locales.set_app(locale)?;
339 let changed = locales.generation() != previous;
340 if changed {
341 let invalidated = self.environment_dependencies.invalidate_locale_app();
342 self.dirty.extend(invalidated);
343 self.mark_all_windows_repaint();
344 }
345 Ok(changed)
346 }
347
348 pub fn set_motion_preference_from_host(&mut self, preference: crate::MotionPreference) -> bool {
354 let previous = self.motions.preference();
355 self.motions.set_preference(preference);
356 let changed = self.motions.preference() != previous;
357 if changed {
358 let now = self.clock.now();
359 self.motion_values = self.motions.snapshot(now);
360 self.mark_all_windows_dirty();
361 }
362 changed
363 }
364
365 pub fn replace_component_styles_from_host(
370 &mut self,
371 styles: crate::ComponentStyleSheet,
372 ) -> bool {
373 if self.component_styles == styles {
374 return false;
375 }
376 self.component_styles = styles;
377 self.component_style_generation = self.component_style_generation.saturating_add(1);
378 self.dirty.extend(self.component_state.paths());
379 true
380 }
381
382 pub(crate) const fn component_style_generation(&self) -> u64 {
383 self.component_style_generation
384 }
385
386 pub(crate) const fn component_styles(&self) -> &crate::ComponentStyleSheet {
387 &self.component_styles
388 }
389
390 pub(crate) fn trace_subscription_closures(&mut self) {
391 for closure in self.subscriptions.take_closures() {
392 let scope = match closure.scope {
393 AsyncScope::App => "/App".to_owned(),
394 AsyncScope::Window(window) => format!("window:{window}"),
395 AsyncScope::Component(component) => component.to_string(),
396 AsyncScope::Effect {
397 component,
398 key,
399 activation,
400 } => format!("{component}/effect[{key}]#{activation}"),
401 };
402 self.traces.push(
403 crate::RuntimeTraceKind::Subscription,
404 scope,
405 format!("close {}: {}", closure.label, closure.reason),
406 None,
407 false,
408 );
409 }
410 }
411
412 pub fn release_window(
418 &mut self,
419 window: &str,
420 root: &ComponentInstancePath,
421 ) -> Result<(), AssetError> {
422 self.tasks
423 .cancel_scope(&AsyncScope::Window(window.to_owned()));
424 self.tasks.cancel_component_scope(root);
425 self.timers.cancel_component_scope(root);
426 self.subscriptions
427 .cancel_scope(&AsyncScope::Window(window.to_owned()));
428 self.subscriptions.cancel_component_scope(root);
429 self.assets.cancel_window_scope(window, root)?;
430 self.stores.remove_window(window);
431 self.native_collections.remove_reader_scope(root);
432 self.native_documents.remove_reader_scope(root);
433 self.component_state.remove_scope(root);
434 self.component_event_handlers
435 .retain(|(path, _), _| !path.is_within(root));
436 self.component_incarnations
437 .retain(|path, _| !path.is_within(root));
438 self.actions.remove_component_scope(root);
439 self.dirty.retain(|path| !path.is_within(root));
440 self.pending_events
441 .retain(|event| !event.target.is_within(root));
442 self.pending_actions.retain(|action| {
443 !action
444 .callback
445 .component()
446 .is_some_and(|path| path.is_within(root))
447 });
448 self.pending_async.retain(|delivery| match &delivery.scope {
449 AsyncScope::App => true,
450 AsyncScope::Window(id) => id != window,
451 AsyncScope::Component(_) | AsyncScope::Effect { .. } => {
452 !delivery.scope.is_within_component(root)
453 }
454 });
455 self.pending_element_commands
456 .retain(|command| command.window() != window);
457 self.environment_dependencies.remove_window(window);
458 self.environment_dependencies.remove_scope(root);
459 self.repaint_windows.remove(window);
460 self.window_appearances.remove(window);
461 if let Some(theme) = self.theme.as_mut() {
462 theme.remove_window(window);
463 theme.remove_scope(root);
464 }
465 if let Some(locale) = self.locale.as_mut() {
466 locale.remove_window(window);
467 locale.remove_scope(root);
468 }
469 let window_scope = format!("window:{window}");
470 self.motions.release_node_scope(&window_scope);
471 let removed_ghosts = self
472 .motion_ghosts
473 .iter()
474 .filter(|ghost| domain_is_within(&ghost.domain, &window_scope))
475 .map(|ghost| ghost.path.clone())
476 .collect::<Vec<_>>();
477 self.motion_ghosts
478 .retain(|ghost| !domain_is_within(&ghost.domain, &window_scope));
479 for path in removed_ghosts {
480 self.motions.cancel_node_scope(&path);
481 }
482 self.motions.discard_timeline_events_in_scope(&window_scope);
483 self.motion_values = self.motions.snapshot(self.clock.now());
484 self.effects.remove_scope(root);
485 self.signals.remove_scope(root);
486 self.element_refs.remove_scope(root);
487 self.windows.remove(window);
488 self.responsive.remove_window(window);
489 Ok(())
490 }
491
492 pub(crate) fn queue_async(
493 &mut self,
494 deliveries: impl IntoIterator<Item = crate::AsyncDelivery>,
495 ) {
496 self.pending_async.extend(deliveries);
497 }
498
499 pub(crate) fn queue_suspended_async(
500 &mut self,
501 deliveries: impl IntoIterator<Item = crate::AsyncDelivery>,
502 ) -> Result<(), AsyncRuntimeError> {
503 for delivery in deliveries {
504 if self.pending_async.len() >= Self::SUSPENDED_DELIVERY_CAPACITY {
505 return Err(AsyncRuntimeError::SuspendedBackpressure {
506 capacity: Self::SUSPENDED_DELIVERY_CAPACITY,
507 });
508 }
509 self.pending_async.push(delivery);
510 }
511 Ok(())
512 }
513
514 pub(crate) fn suspended_delivery_capacity_remaining(&self) -> usize {
515 Self::SUSPENDED_DELIVERY_CAPACITY.saturating_sub(self.pending_async.len())
516 }
517
518 pub(crate) fn cancel_async_scope(&mut self, scope: &AsyncScope) -> Result<(), AssetError> {
519 self.assets.cancel_scope(scope)?;
520 self.tasks.cancel_scope(scope);
521 self.subscriptions.cancel_scope(scope);
522 self.pending_async
523 .retain(|delivery| &delivery.scope != scope);
524 self.trace_subscription_closures();
525 Ok(())
526 }
527
528 pub(crate) fn take_window_async(
529 &mut self,
530 window: &str,
531 root: &ComponentInstancePath,
532 ) -> Vec<crate::AsyncDelivery> {
533 let app_owner = self.windows.open_ids().into_iter().next();
534 let pending = std::mem::take(&mut self.pending_async);
535 let (accepted, retained) =
536 pending
537 .into_iter()
538 .partition(|delivery| match &delivery.scope {
539 AsyncScope::App => app_owner.as_deref() == Some(window),
540 AsyncScope::Window(id) => id == window,
541 AsyncScope::Component(_) | AsyncScope::Effect { .. } => {
542 delivery.scope.is_within_component(root)
543 }
544 });
545 self.pending_async = retained;
546 accepted
547 }
548
549 pub(crate) fn discard_component_async_before_generation(
550 &mut self,
551 root: &ComponentInstancePath,
552 generation: crate::ScriptGeneration,
553 ) {
554 self.pending_async.retain(|delivery| {
555 !delivery.scope.is_within_component(root)
556 || delivery.callback.generation() == generation
557 });
558 }
559
560 pub(crate) fn take_window_element_commands(
561 &mut self,
562 window: &str,
563 ) -> Vec<crate::element_ref::ElementCommand> {
564 let commands = std::mem::take(&mut self.pending_element_commands);
565 let (selected, retained) = commands
566 .into_iter()
567 .partition(|command| command.window() == window);
568 self.pending_element_commands = retained;
569 selected
570 }
571
572 pub(crate) fn has_window_dirty(&self, root: &ComponentInstancePath) -> bool {
573 self.dirty.iter().any(|path| path.is_within(root))
574 }
575
576 pub(crate) fn flush_geometry_dependencies(&mut self) {
577 self.dirty.extend(self.geometry.take_dirty());
578 for presentation in self.presentations.values() {
579 self.dirty.extend(presentation.geometry.take_dirty());
580 }
581 }
582
583 pub(crate) fn take_window_dirty_components(
584 &mut self,
585 root: &ComponentInstancePath,
586 ) -> BTreeSet<ComponentInstancePath> {
587 let all = std::mem::take(&mut self.dirty);
588 let (selected, retained) = all.into_iter().partition(|path| path.is_within(root));
589 self.dirty = retained;
590 selected
591 }
592
593 pub(crate) fn drain_pending_actions(&mut self) -> Vec<ActionInvocation> {
594 std::mem::take(&mut self.pending_actions)
595 }
596
597 pub(crate) fn drain_pending_events(&mut self) -> Vec<PendingEvent> {
598 std::mem::take(&mut self.pending_events)
599 }
600
601 pub(crate) fn has_pending_dispatch(&self) -> bool {
602 !self.pending_actions.is_empty() || !self.pending_events.is_empty()
603 }
604
605 pub(crate) fn has_virtual_requests(&self) -> bool {
606 !self.virtual_requests.is_empty()
607 }
608
609 pub(crate) fn component_event_handler(
610 &self,
611 component: &ComponentInstancePath,
612 event: &str,
613 ) -> Option<ScriptCallback> {
614 self.component_event_handlers
615 .get(&(component.clone(), event.to_owned()))
616 .cloned()
617 }
618
619 pub(crate) fn reset_component_readers(&mut self, component: &ComponentInstancePath) {
620 self.stores.reset_reader(component);
621 self.native_collections.reset_reader(component);
622 self.native_documents.reset_reader(component);
623 self.environment_dependencies.reset_reader(component);
624 }
625
626 pub(crate) fn component_event_handlers_in_scope(
627 &self,
628 root: &ComponentInstancePath,
629 ) -> BTreeMap<(ComponentInstancePath, String), ScriptCallback> {
630 self.component_event_handlers
631 .iter()
632 .filter(|((path, _), _)| path.is_within(root))
633 .map(|(id, callback)| (id.clone(), callback.clone()))
634 .collect()
635 }
636
637 pub(crate) fn replace_component_event_handlers(
638 &mut self,
639 root: &ComponentInstancePath,
640 handlers: BTreeMap<(ComponentInstancePath, String), ScriptCallback>,
641 ) {
642 self.component_event_handlers
643 .retain(|(path, _), _| !path.is_within(root));
644 self.component_event_handlers.extend(handlers);
645 }
646
647 pub(crate) fn mark_all_windows_dirty(&mut self) {
648 self.dirty.extend(
649 self.windows
650 .open_ids()
651 .into_iter()
652 .map(|window| ComponentInstancePath::root("App", window)),
653 );
654 }
655
656 pub(crate) fn mark_dirty(
657 &mut self,
658 components: impl IntoIterator<Item = ComponentInstancePath>,
659 ) {
660 self.dirty.extend(components);
661 }
662
663 pub(crate) fn mark_all_windows_repaint(&mut self) {
664 self.repaint_windows.extend(self.windows.open_ids());
665 }
666
667 pub(crate) fn mark_window_repaint(&mut self, window: impl Into<String>) {
668 self.repaint_windows.insert(window.into());
669 }
670
671 pub(crate) fn take_window_repaint(&mut self, window: &str) -> bool {
672 self.repaint_windows.remove(window)
673 }
674
675 pub(crate) fn reconcile_component_lifetimes(
676 &mut self,
677 root: &ComponentInstancePath,
678 active: &BTreeSet<ComponentInstancePath>,
679 previous: &BTreeSet<ComponentInstancePath>,
680 ) -> Result<(), AssetError> {
681 for removed in previous
682 .iter()
683 .filter(|path| path.is_within(root) && *path != root && !active.contains(*path))
684 {
685 self.tasks.cancel_component_scope(removed);
686 self.subscriptions.cancel_component_scope(removed);
687 self.timers.cancel_component_scope(removed);
688 self.assets.cancel_component_scope(removed)?;
689 self.actions.remove_component_scope(removed);
690 }
691 self.pending_async.retain(|delivery| {
692 delivery
693 .scope
694 .component()
695 .is_none_or(|path| !path.is_within(root) || active.contains(path))
696 });
697 self.pending_actions.retain(|action| {
698 !action
699 .callback
700 .component()
701 .is_some_and(|path| path.is_within(root) && !active.contains(path))
702 });
703 self.pending_events.retain(|event| {
704 !event.target.is_within(root) || active.contains(&event.target) || event.target == *root
705 });
706 self.stores.retain_reader_scope(root, active);
707 self.native_collections.retain_reader_scope(root, active);
708 self.native_documents.retain_reader_scope(root, active);
709 self.environment_dependencies.retain_scope(root, active);
710 self.dirty
711 .retain(|path| !path.is_within(root) || path == root || active.contains(path));
712 self.component_incarnations
713 .retain(|path, _| !path.is_within(root) || path == root || active.contains(path));
714 Ok(())
715 }
716
717 #[must_use]
718 pub fn drain_batch(&mut self) -> UiMutationBatch {
719 UiMutationBatch {
720 dirty: std::mem::take(&mut self.dirty),
721 events: std::mem::take(&mut self.pending_events),
722 actions: std::mem::take(&mut self.pending_actions),
723 }
724 }
725
726 #[must_use]
727 pub fn dirty_components(&self) -> &BTreeSet<ComponentInstancePath> {
728 &self.dirty
729 }
730
731 pub(crate) fn snapshot(&self) -> Result<UiStateSnapshot, AssetError> {
732 Ok(UiStateSnapshot {
733 runtime_id: self.runtime_id,
734 transaction_depth: self.transaction_depth,
735 component_state: self.component_state.clone(),
736 stores: self.stores.clone(),
737 native_collections: self.native_collections.clone(),
738 native_documents: self.native_documents.clone(),
739 actions: self.actions.clone(),
740 component_event_handlers: self.component_event_handlers.clone(),
741 component_incarnations: self.component_incarnations.clone(),
742 next_component_incarnation: self.next_component_incarnation,
743 dirty: self.dirty.clone(),
744 pending_events: self.pending_events.clone(),
745 pending_actions: self.pending_actions.clone(),
746 pending_async: self.pending_async.clone(),
747 pending_element_commands: self.pending_element_commands.clone(),
748 locale: self.locale.clone(),
749 theme: self.theme.clone(),
750 component_styles: self.component_styles.clone(),
751 component_style_generation: self.component_style_generation,
752 motions: self.motions.transaction_snapshot(),
753 effects: self.effects.clone(),
754 signals: self.signals.clone(),
755 element_refs: self.element_refs.clone(),
756 geometry: self.geometry.snapshot(),
757 pointer_capture: self.pointer_capture.snapshot(),
758 presentations: self
759 .presentations
760 .iter()
761 .map(|(view, state)| (view.clone(), state.snapshot()))
762 .collect(),
763 budgets: self.budgets.clone(),
764 virtual_requests: self.virtual_requests.snapshot(),
765 motion_values: self.motion_values.clone(),
766 motion_ghosts: self.motion_ghosts.clone(),
767 windows: self.windows.clone(),
768 responsive: self.responsive.clone(),
769 environment_dependencies: self.environment_dependencies.clone(),
770 repaint_windows: self.repaint_windows.clone(),
771 tasks: self.tasks.snapshot(),
772 subscriptions: self.subscriptions.snapshot(),
773 timers: self.timers.clone(),
774 image_decodes: self.assets.decode_snapshot()?,
775 })
776 }
777
778 pub(crate) fn restore(&mut self, snapshot: UiStateSnapshot) -> Result<(), UiTransactionError> {
779 if self.runtime_id != snapshot.runtime_id {
780 return Err(UiTransactionError::ForeignSnapshot);
781 }
782 let expected_depth = snapshot.transaction_depth.saturating_add(1);
783 if self.transaction_depth != expected_depth {
784 return Err(UiTransactionError::Unbalanced {
785 expected: expected_depth,
786 actual: self.transaction_depth,
787 });
788 }
789 self.tasks.restore(snapshot.tasks);
790 self.subscriptions.restore(snapshot.subscriptions);
791 self.timers = snapshot.timers;
792 let decode_result = self.assets.restore_decode_snapshot(snapshot.image_decodes);
793 self.component_state = snapshot.component_state;
794 self.stores = snapshot.stores;
795 self.native_collections = snapshot.native_collections;
796 self.native_documents = snapshot.native_documents;
797 self.actions = snapshot.actions;
798 self.component_event_handlers = snapshot.component_event_handlers;
799 self.component_incarnations = snapshot.component_incarnations;
800 self.next_component_incarnation = snapshot.next_component_incarnation;
801 self.dirty = snapshot.dirty;
802 self.pending_events = snapshot.pending_events;
803 self.pending_actions = snapshot.pending_actions;
804 self.pending_async = snapshot.pending_async;
805 self.pending_element_commands = snapshot.pending_element_commands;
806 self.locale = snapshot.locale;
807 self.theme = snapshot.theme;
808 self.component_styles = snapshot.component_styles;
809 self.component_style_generation = snapshot.component_style_generation;
810 self.motions = snapshot.motions;
811 self.effects = snapshot.effects;
812 self.signals = snapshot.signals;
813 self.element_refs = snapshot.element_refs;
814 self.geometry.restore(snapshot.geometry);
815 self.pointer_capture.restore(snapshot.pointer_capture);
816 self.presentations
817 .retain(|view, _| snapshot.presentations.contains_key(view));
818 for (view, presentation) in snapshot.presentations {
819 let state = self.presentations.entry(view).or_default();
820 state.geometry.restore(presentation.geometry);
821 state.pointer_capture.restore(presentation.pointer_capture);
822 }
823 self.budgets = snapshot.budgets;
824 self.virtual_requests.restore(snapshot.virtual_requests);
825 self.motion_values = snapshot.motion_values;
826 self.motion_ghosts = snapshot.motion_ghosts;
827 self.windows = snapshot.windows;
828 self.responsive = snapshot.responsive;
829 self.environment_dependencies = snapshot.environment_dependencies;
830 self.repaint_windows = snapshot.repaint_windows;
831 self.transaction_depth = snapshot.transaction_depth;
832 decode_result.map_err(UiTransactionError::from)
833 }
834
835 pub fn begin_transaction(&mut self) -> Result<UiStateSnapshot, UiTransactionError> {
844 let snapshot = self.snapshot()?;
845 let _ = self.assets.begin_transaction()?;
846 self.tasks.begin_transaction();
847 self.subscriptions.begin_transaction();
848 self.transaction_depth = self.transaction_depth.saturating_add(1);
849 Ok(snapshot)
850 }
851
852 pub fn commit_transaction(&mut self) -> Result<(), UiTransactionError> {
859 if self.transaction_depth == 0 {
860 return Err(UiTransactionError::Unbalanced {
861 expected: 1,
862 actual: 0,
863 });
864 }
865 self.assets.commit_transaction()?;
866 self.tasks.commit_transaction();
867 self.subscriptions.commit_transaction();
868 self.transaction_depth -= 1;
869 Ok(())
870 }
871
872 pub fn rollback_transaction(
879 &mut self,
880 snapshot: UiStateSnapshot,
881 ) -> Result<(), UiTransactionError> {
882 self.restore(snapshot)
883 }
884}
885
886#[derive(Debug, Default)]
887struct ViewPresentationState {
888 geometry: crate::GeometryRegistry,
889 pointer_capture: crate::PointerCaptureRegistry,
890}
891
892impl ViewPresentationState {
893 fn snapshot(&self) -> ViewPresentationSnapshot {
894 ViewPresentationSnapshot {
895 geometry: self.geometry.snapshot(),
896 pointer_capture: self.pointer_capture.snapshot(),
897 }
898 }
899}
900
901#[derive(Clone, Debug)]
902struct ViewPresentationSnapshot {
903 geometry: crate::geometry::GeometrySnapshot,
904 pointer_capture: BTreeMap<u64, crate::NodeId>,
905}
906
907#[derive(Debug, Error)]
908pub enum UiTransactionError {
909 #[error("transaction snapshot belongs to another UiRuntimeState")]
910 ForeignSnapshot,
911 #[error("unbalanced runtime transaction depth: expected {expected}, got {actual}")]
912 Unbalanced { expected: usize, actual: usize },
913 #[error(transparent)]
914 Asset(#[from] AssetError),
915}
916
917pub struct UiStateSnapshot {
918 runtime_id: RuntimeStateId,
919 transaction_depth: usize,
920 component_state: StateStore,
921 stores: StoreRegistry,
922 native_collections: crate::NativeCollectionRegistry,
923 native_documents: crate::NativeTextDocumentRegistry,
924 actions: ActionRegistry,
925 component_event_handlers: BTreeMap<(ComponentInstancePath, String), ScriptCallback>,
926 component_incarnations: BTreeMap<ComponentInstancePath, ComponentIncarnation>,
927 next_component_incarnation: u64,
928 dirty: BTreeSet<ComponentInstancePath>,
929 pending_events: Vec<PendingEvent>,
930 pending_actions: Vec<ActionInvocation>,
931 pending_async: Vec<crate::AsyncDelivery>,
932 pending_element_commands: Vec<crate::element_ref::ElementCommand>,
933 locale: Option<LocaleManager>,
934 theme: Option<ThemeManager>,
935 component_styles: crate::ComponentStyleSheet,
936 component_style_generation: u64,
937 motions: MotionRuntime,
938 effects: crate::EffectRegistry,
939 signals: crate::SignalRegistry,
940 element_refs: crate::ElementRefRegistry,
941 geometry: crate::geometry::GeometrySnapshot,
942 pointer_capture: BTreeMap<u64, crate::NodeId>,
943 presentations: BTreeMap<String, ViewPresentationSnapshot>,
944 budgets: crate::RuntimeBudgets,
945 virtual_requests: crate::virtual_list::VirtualRequestSnapshot,
946 motion_values: BTreeMap<MotionKey, f64>,
947 motion_ghosts: Vec<crate::motion::MotionGhost>,
948 windows: WindowCommandRegistry,
949 responsive: ResponsiveRuntime,
950 environment_dependencies: crate::environment_dependency::EnvironmentDependencyRegistry,
951 repaint_windows: BTreeSet<String>,
952 tasks: crate::async_runtime::TaskRegistrySnapshot,
953 subscriptions: crate::async_runtime::SubscriptionRegistrySnapshot,
954 timers: crate::TimerRegistry,
955 image_decodes: crate::asset::ImageDecodeSnapshot,
956}
957
958impl Clone for UiStateSnapshot {
959 fn clone(&self) -> Self {
960 Self {
961 runtime_id: self.runtime_id,
962 transaction_depth: self.transaction_depth,
963 component_state: self.component_state.clone(),
964 stores: self.stores.clone(),
965 native_collections: self.native_collections.clone(),
966 native_documents: self.native_documents.clone(),
967 actions: self.actions.clone(),
968 component_event_handlers: self.component_event_handlers.clone(),
969 component_incarnations: self.component_incarnations.clone(),
970 next_component_incarnation: self.next_component_incarnation,
971 dirty: self.dirty.clone(),
972 pending_events: self.pending_events.clone(),
973 pending_actions: self.pending_actions.clone(),
974 pending_async: self.pending_async.clone(),
975 pending_element_commands: self.pending_element_commands.clone(),
976 locale: self.locale.clone(),
977 theme: self.theme.clone(),
978 component_styles: self.component_styles.clone(),
979 component_style_generation: self.component_style_generation,
980 motions: self.motions.transaction_snapshot(),
981 effects: self.effects.clone(),
982 signals: self.signals.clone(),
983 element_refs: self.element_refs.clone(),
984 geometry: self.geometry.clone(),
985 pointer_capture: self.pointer_capture.clone(),
986 presentations: self.presentations.clone(),
987 budgets: self.budgets.clone(),
988 virtual_requests: self.virtual_requests.clone(),
989 motion_values: self.motion_values.clone(),
990 motion_ghosts: self.motion_ghosts.clone(),
991 windows: self.windows.clone(),
992 responsive: self.responsive.clone(),
993 environment_dependencies: self.environment_dependencies.clone(),
994 repaint_windows: self.repaint_windows.clone(),
995 tasks: self.tasks.clone(),
996 subscriptions: self.subscriptions.clone(),
997 timers: self.timers.clone(),
998 image_decodes: self.image_decodes.clone(),
999 }
1000 }
1001}
1002
1003impl UiStateSnapshot {
1004 pub(crate) const fn component_state(&self) -> &StateStore {
1005 &self.component_state
1006 }
1007}
1008
1009#[derive(Clone, Debug)]
1010pub struct PendingEvent {
1011 pub target: ComponentInstancePath,
1012 pub event: UiEvent,
1013}
1014
1015#[derive(Clone, Debug, Default)]
1016pub struct UiMutationBatch {
1017 pub dirty: BTreeSet<ComponentInstancePath>,
1018 pub events: Vec<PendingEvent>,
1019 pub actions: Vec<ActionInvocation>,
1020}
1021
1022#[derive(Clone, Debug)]
1023pub struct UiContext {
1024 runtime: Rc<RefCell<UiRuntimeState>>,
1025 component: ComponentInstancePath,
1026 incarnation: ComponentIncarnation,
1027 window: Option<String>,
1028 view: Option<String>,
1029 phase: ExecutionPhase,
1030 events: BTreeMap<String, EventSchema>,
1031 generation: ScriptGeneration,
1032 native_context: Option<crate::invocation::ScriptInvocationContext>,
1033 async_scope: Option<AsyncScope>,
1034 component_style: Option<crate::Style>,
1035 component_part_styles: BTreeMap<String, crate::Style>,
1036 global_component_part_styles: BTreeMap<String, crate::Style>,
1037 non_reusable_render_reads: Rc<RefCell<BTreeSet<ComponentInstancePath>>>,
1038 event_target: Option<crate::GeometryBounds>,
1039}
1040
1041impl UiContext {
1042 #[must_use]
1043 pub fn new(
1044 runtime: Rc<RefCell<UiRuntimeState>>,
1045 component: ComponentInstancePath,
1046 window: Option<String>,
1047 phase: ExecutionPhase,
1048 events: BTreeMap<String, EventSchema>,
1049 ) -> Self {
1050 if let Some(window) = window.as_deref()
1051 && let Ok(mut runtime) = runtime.try_borrow_mut()
1052 {
1053 runtime.ensure_presentation(window);
1054 }
1055 let incarnation = runtime
1056 .try_borrow_mut()
1057 .map(|mut runtime| runtime.ensure_component_incarnation(&component))
1058 .unwrap_or_default();
1059 let context = Self {
1060 runtime,
1061 component,
1062 incarnation,
1063 window,
1064 view: None,
1065 phase,
1066 events,
1067 generation: ScriptGeneration::default(),
1068 native_context: None,
1069 async_scope: None,
1070 component_style: None,
1071 component_part_styles: BTreeMap::new(),
1072 global_component_part_styles: BTreeMap::new(),
1073 non_reusable_render_reads: Rc::new(RefCell::new(BTreeSet::new())),
1074 event_target: None,
1075 };
1076 if phase == ExecutionPhase::Render
1077 && let Ok(mut runtime) = context.runtime.try_borrow_mut()
1078 {
1079 runtime.reset_component_readers(&context.component);
1080 }
1081 context
1082 }
1083
1084 #[must_use]
1085 pub fn with_generation(mut self, generation: ScriptGeneration) -> Self {
1086 self.generation = generation;
1087 self
1088 }
1089
1090 #[must_use]
1091 pub fn with_view_id(mut self, view: impl Into<String>) -> Self {
1092 let view = view.into();
1093 if let Ok(mut runtime) = self.runtime.try_borrow_mut() {
1094 runtime.ensure_presentation(&view);
1095 }
1096 self.view = Some(view);
1097 self
1098 }
1099
1100 #[must_use]
1101 pub(crate) fn with_optional_view_id(mut self, view: Option<String>) -> Self {
1102 if let Some(view) = view.as_deref()
1103 && let Ok(mut runtime) = self.runtime.try_borrow_mut()
1104 {
1105 runtime.ensure_presentation(view);
1106 }
1107 self.view = view;
1108 self
1109 }
1110
1111 pub(crate) fn for_component(
1112 &self,
1113 component: ComponentInstancePath,
1114 events: BTreeMap<String, EventSchema>,
1115 ) -> Self {
1116 let incarnation = self
1117 .runtime
1118 .try_borrow_mut()
1119 .map(|mut runtime| runtime.ensure_component_incarnation(&component))
1120 .unwrap_or_default();
1121 let context = Self {
1122 runtime: Rc::clone(&self.runtime),
1123 component,
1124 incarnation,
1125 window: self.window.clone(),
1126 view: self.view.clone(),
1127 phase: self.phase,
1128 events,
1129 generation: self.generation,
1130 native_context: self.native_context.clone(),
1131 async_scope: self.async_scope.clone(),
1132 component_style: self.component_style.clone(),
1133 component_part_styles: self.component_part_styles.clone(),
1134 global_component_part_styles: self.global_component_part_styles.clone(),
1135 non_reusable_render_reads: Rc::clone(&self.non_reusable_render_reads),
1136 event_target: self.event_target,
1137 };
1138 if context.phase == ExecutionPhase::Render
1139 && let Ok(mut runtime) = context.runtime.try_borrow_mut()
1140 {
1141 runtime.reset_component_readers(&context.component);
1142 }
1143 context
1144 }
1145
1146 pub(crate) fn with_native_context(
1147 mut self,
1148 native_context: Option<crate::invocation::ScriptInvocationContext>,
1149 ) -> Self {
1150 self.native_context = native_context;
1151 self
1152 }
1153
1154 pub(crate) const fn with_event_target(mut self, target: Option<crate::GeometryBounds>) -> Self {
1155 self.event_target = target;
1156 self
1157 }
1158
1159 pub(crate) fn with_async_scope(mut self, scope: AsyncScope) -> Self {
1160 self.async_scope = Some(scope);
1161 self
1162 }
1163
1164 pub(crate) fn with_component_styles(
1165 mut self,
1166 global_part_styles: BTreeMap<String, crate::Style>,
1167 style: Option<crate::Style>,
1168 part_styles: BTreeMap<String, crate::Style>,
1169 ) -> Self {
1170 self.global_component_part_styles = global_part_styles;
1171 self.component_style = style;
1172 self.component_part_styles = part_styles;
1173 self
1174 }
1175
1176 pub(crate) fn reset_non_reusable_render_reads(&self) {
1177 self.non_reusable_render_reads.borrow_mut().clear();
1178 }
1179
1180 pub(crate) fn component_render_is_reusable(&self) -> bool {
1181 !self
1182 .non_reusable_render_reads
1183 .borrow()
1184 .contains(&self.component)
1185 }
1186
1187 fn mark_non_reusable_render_read(&self) {
1188 if self.phase == ExecutionPhase::Render {
1189 self.non_reusable_render_reads
1190 .borrow_mut()
1191 .insert(self.component.clone());
1192 }
1193 }
1194
1195 fn resolve_component_style(&self, part: &str, mut base: crate::Style) -> crate::Style {
1196 if let Some(style) = self.global_component_part_styles.get(part) {
1197 base = base.merged(style);
1198 }
1199 if part == "root"
1200 && let Some(style) = &self.component_style
1201 {
1202 base = base.merged(style);
1203 }
1204 if let Some(style) = self.component_part_styles.get(part) {
1205 base = base.merged(style);
1206 }
1207 base
1208 }
1209
1210 fn scoped_callback(&self, function: FnPtr) -> Result<ScriptCallback, UiContextError> {
1211 let mut callback = ScriptCallback::try_from_fn_ptr(function, self.generation)?;
1212 callback.bind_component_scope_if_unset(
1213 &self.component,
1214 self.incarnation,
1215 self.events.clone(),
1216 );
1217 if let Some(context) = &self.native_context {
1218 callback.bind_native_context_if_unset(context.clone());
1219 }
1220 Ok(callback)
1221 }
1222
1223 #[must_use]
1224 pub fn runtime(&self) -> &Rc<RefCell<UiRuntimeState>> {
1225 &self.runtime
1226 }
1227
1228 pub(crate) fn component_path(&self) -> &ComponentInstancePath {
1229 &self.component
1230 }
1231
1232 pub(crate) const fn component_incarnation(&self) -> ComponentIncarnation {
1233 self.incarnation
1234 }
1235
1236 pub(crate) fn event_schemas(&self) -> &BTreeMap<String, EventSchema> {
1237 &self.events
1238 }
1239
1240 pub(crate) fn native_context(&self) -> Option<&crate::invocation::ScriptInvocationContext> {
1241 self.native_context.as_ref()
1242 }
1243
1244 pub fn get_state(&self, field: &str) -> Result<UiValue, UiContextError> {
1251 self.runtime
1252 .try_borrow()
1253 .map_err(|_| UiContextError::Borrowed)?
1254 .component_state
1255 .get(&self.component, field)
1256 .cloned()
1257 .ok_or_else(|| UiContextError::UnknownState {
1258 component: self.component.clone(),
1259 field: field.to_owned(),
1260 })
1261 }
1262
1263 pub fn get_state_path(
1273 &self,
1274 field: &str,
1275 path: &UiValuePath,
1276 ) -> Result<UiValue, UiContextError> {
1277 Ok(self.get_state(field)?.get_path(path)?.clone())
1278 }
1279
1280 pub fn set_state(&self, field: &str, value: Dynamic) -> Result<(), UiContextError> {
1287 self.require_mutation()?;
1288 let value = UiValue::from_dynamic(value)?;
1289 let mut runtime = self
1290 .runtime
1291 .try_borrow_mut()
1292 .map_err(|_| UiContextError::Borrowed)?;
1293 let sensitive = runtime.component_state.is_sensitive(&self.component, field);
1294 let changed = runtime
1295 .component_state
1296 .set(&self.component, field, value.clone())?;
1297 if changed {
1298 runtime.dirty.insert(self.component.clone());
1299 }
1300 runtime.traces.push(
1301 crate::RuntimeTraceKind::State,
1302 self.component.to_string(),
1303 format!("set {field}"),
1304 Some(value),
1305 sensitive,
1306 );
1307 Ok(())
1308 }
1309
1310 pub fn set_state_path(
1317 &self,
1318 field: &str,
1319 path: &UiValuePath,
1320 value: Dynamic,
1321 ) -> Result<(), UiContextError> {
1322 self.require_mutation()?;
1323 let value = UiValue::from_dynamic(value)?;
1324 let mut runtime = self
1325 .runtime
1326 .try_borrow_mut()
1327 .map_err(|_| UiContextError::Borrowed)?;
1328 let mut root = runtime
1329 .component_state
1330 .get(&self.component, field)
1331 .cloned()
1332 .ok_or_else(|| UiContextError::UnknownState {
1333 component: self.component.clone(),
1334 field: field.to_owned(),
1335 })?;
1336 root.set_path(path, value.clone())?;
1337 let sensitive = runtime.component_state.is_sensitive(&self.component, field);
1338 if runtime.component_state.set(&self.component, field, root)? {
1339 runtime.dirty.insert(self.component.clone());
1340 }
1341 runtime.traces.push(
1342 crate::RuntimeTraceKind::State,
1343 self.component.to_string(),
1344 format!("set nested {field}"),
1345 Some(value),
1346 sensitive,
1347 );
1348 Ok(())
1349 }
1350
1351 pub fn get_signal(&self, signal: &crate::NativeSignal) -> Result<Dynamic, UiContextError> {
1357 self.mark_non_reusable_render_read();
1358 Ok(self
1359 .runtime
1360 .try_borrow()
1361 .map_err(|_| UiContextError::Borrowed)?
1362 .signals
1363 .read(signal)?
1364 .into_dynamic())
1365 }
1366
1367 pub fn set_signal(
1374 &self,
1375 signal: &crate::NativeSignal,
1376 value: Dynamic,
1377 ) -> Result<(), UiContextError> {
1378 self.require_mutation()?;
1379 let value = crate::SignalValue::from_dynamic_for_kind(value, signal.id().kind())?;
1380 let mut runtime = self
1381 .runtime
1382 .try_borrow_mut()
1383 .map_err(|_| UiContextError::Borrowed)?;
1384 let changed =
1385 runtime
1386 .signals
1387 .write_from(signal, value.clone(), crate::SignalWriter::Script)?;
1388 runtime.traces.push(
1389 crate::RuntimeTraceKind::Signal,
1390 signal.id().component().to_string(),
1391 format!(
1392 "set {} ({}){}",
1393 signal.id().key(),
1394 signal.id().kind().as_str(),
1395 if changed { "" } else { " unchanged" }
1396 ),
1397 None,
1398 false,
1399 );
1400 Ok(())
1401 }
1402
1403 pub fn get_signal_by_key(&self, key: &str) -> Result<Dynamic, UiContextError> {
1409 self.mark_non_reusable_render_read();
1410 let runtime = self
1411 .runtime
1412 .try_borrow()
1413 .map_err(|_| UiContextError::Borrowed)?;
1414 let signal = runtime.signals.resolve(&self.component, key)?;
1415 Ok(runtime.signals.read(&signal)?.into_dynamic())
1416 }
1417
1418 pub(crate) fn parent_optional_float_signal_ref(
1427 &self,
1428 key: &str,
1429 ) -> Result<crate::NativeSignal, UiContextError> {
1430 let runtime = self
1431 .runtime
1432 .try_borrow()
1433 .map_err(|_| UiContextError::Borrowed)?;
1434 let mut scope = self.component.parent();
1435 while let Some(component) = scope {
1436 if let Ok(signal) = runtime.signals.resolve(&component, key) {
1437 if signal.id().kind() == crate::SignalKind::OptionalFloat {
1438 return Ok(signal);
1439 }
1440 return Err(crate::SignalError::TypeMismatch {
1441 expected: crate::SignalKind::OptionalFloat,
1442 actual: signal.id().kind(),
1443 }
1444 .into());
1445 }
1446 if let Some(incarnation) = runtime.component_incarnation(&component) {
1447 let id = crate::SignalId::new_scoped(
1448 component,
1449 incarnation,
1450 key,
1451 crate::SignalKind::OptionalFloat,
1452 )?;
1453 return Ok(crate::NativeSignal::new(id));
1454 }
1455 scope = component.parent();
1456 }
1457 Err(crate::SignalError::UnknownKey {
1458 component: self.component.clone(),
1459 key: key.to_owned(),
1460 }
1461 .into())
1462 }
1463
1464 pub fn set_signal_by_key(&self, key: &str, value: Dynamic) -> Result<(), UiContextError> {
1470 self.require_mutation()?;
1471 let signal = self
1472 .runtime
1473 .try_borrow()
1474 .map_err(|_| UiContextError::Borrowed)?
1475 .signals
1476 .resolve(&self.component, key)?;
1477 self.set_signal(&signal, value)
1478 }
1479
1480 pub fn element_bounds(&self, reference: &crate::ElementRef) -> Result<UiValue, UiContextError> {
1489 let mut runtime = self
1490 .runtime
1491 .try_borrow_mut()
1492 .map_err(|_| UiContextError::Borrowed)?;
1493 let node = if self.phase == ExecutionPhase::Render {
1494 runtime
1495 .element_refs
1496 .resolve_and_track_geometry(reference, &self.component)
1497 } else {
1498 Some(runtime.element_refs.resolve(reference)?)
1499 };
1500 let Some(node) = node else {
1501 return Ok(UiValue::Null);
1502 };
1503 let geometry_registry =
1504 runtime.geometry_for(self.view.as_deref().or(self.window.as_deref()));
1505 drop(runtime);
1506 let Some(geometry) = geometry_registry.read_tracked(node, &self.component) else {
1507 return Ok(UiValue::Null);
1508 };
1509 Ok(UiValue::Map(BTreeMap::from([
1510 ("layout".to_owned(), geometry_bounds_value(geometry.layout)),
1511 ("visual".to_owned(), geometry_bounds_value(geometry.visual)),
1512 (
1513 "clip".to_owned(),
1514 geometry.clip.map_or(UiValue::Null, geometry_bounds_value),
1515 ),
1516 ])))
1517 }
1518
1519 pub fn element_bounds_by_key(&self, key: &str) -> Result<UiValue, UiContextError> {
1528 let reference = self
1529 .runtime
1530 .try_borrow()
1531 .map_err(|_| UiContextError::Borrowed)?
1532 .element_refs
1533 .resolve_key(&self.component, key)?;
1534 self.element_bounds(&reference)
1535 }
1536
1537 pub fn event_target_bounds(&self) -> Result<UiValue, UiContextError> {
1548 if self.phase != ExecutionPhase::Event {
1549 return Err(UiContextError::EventTargetOutsideEvent);
1550 }
1551 Ok(self
1552 .event_target
1553 .map_or(UiValue::Null, crate::GeometryBounds::into_value))
1554 }
1555
1556 pub fn focus_element(&self, reference: &crate::ElementRef) -> Result<(), UiContextError> {
1562 self.require_mutation()?;
1563 let window = self.window.clone().ok_or(UiContextError::MissingWindow)?;
1564 let mut runtime = self
1565 .runtime
1566 .try_borrow_mut()
1567 .map_err(|_| UiContextError::Borrowed)?;
1568 let node = runtime.element_refs.resolve(reference)?;
1569 runtime
1570 .pending_element_commands
1571 .push(crate::element_ref::ElementCommand::Focus { window, node });
1572 Ok(())
1573 }
1574
1575 pub fn focus_element_by_key(&self, key: &str) -> Result<(), UiContextError> {
1581 let reference = self
1582 .runtime
1583 .try_borrow()
1584 .map_err(|_| UiContextError::Borrowed)?
1585 .element_refs
1586 .resolve_key(&self.component, key)?;
1587 self.focus_element(&reference)
1588 }
1589
1590 pub fn scroll_element_to(
1597 &self,
1598 reference: &crate::ElementRef,
1599 x: f64,
1600 y: f64,
1601 ) -> Result<(), UiContextError> {
1602 self.require_mutation()?;
1603 if !x.is_finite() || !y.is_finite() || x < 0.0 || y < 0.0 {
1604 return Err(UiContextError::InvalidScrollOffset { x, y });
1605 }
1606 let window = self.window.clone().ok_or(UiContextError::MissingWindow)?;
1607 let mut runtime = self
1608 .runtime
1609 .try_borrow_mut()
1610 .map_err(|_| UiContextError::Borrowed)?;
1611 let node = runtime.element_refs.resolve(reference)?;
1612 runtime
1613 .pending_element_commands
1614 .push(crate::element_ref::ElementCommand::ScrollTo { window, node, x, y });
1615 Ok(())
1616 }
1617
1618 pub fn scroll_element_to_by_key(
1624 &self,
1625 key: &str,
1626 x: f64,
1627 y: f64,
1628 ) -> Result<(), UiContextError> {
1629 let reference = self
1630 .runtime
1631 .try_borrow()
1632 .map_err(|_| UiContextError::Borrowed)?
1633 .element_refs
1634 .resolve_key(&self.component, key)?;
1635 self.scroll_element_to(&reference, x, y)
1636 }
1637
1638 pub fn scroll_element_into_view(
1644 &self,
1645 reference: &crate::ElementRef,
1646 ) -> Result<(), UiContextError> {
1647 self.require_mutation()?;
1648 let window = self.window.clone().ok_or(UiContextError::MissingWindow)?;
1649 let mut runtime = self
1650 .runtime
1651 .try_borrow_mut()
1652 .map_err(|_| UiContextError::Borrowed)?;
1653 let node = runtime.element_refs.resolve(reference)?;
1654 runtime
1655 .pending_element_commands
1656 .push(crate::element_ref::ElementCommand::ScrollIntoView { window, node });
1657 Ok(())
1658 }
1659
1660 pub fn scroll_element_into_view_by_key(&self, key: &str) -> Result<(), UiContextError> {
1666 let reference = self
1667 .runtime
1668 .try_borrow()
1669 .map_err(|_| UiContextError::Borrowed)?
1670 .element_refs
1671 .resolve_key(&self.component, key)?;
1672 self.scroll_element_into_view(&reference)
1673 }
1674
1675 pub fn get_app_store(&self, store: &str, field: &str) -> Result<UiValue, UiContextError> {
1681 let mut runtime = self
1682 .runtime
1683 .try_borrow_mut()
1684 .map_err(|_| UiContextError::Borrowed)?;
1685 Ok(runtime
1686 .stores
1687 .read_tracked(&self.component, &StoreId::app(store), field)?)
1688 }
1689
1690 pub fn get_native_collection(
1699 &self,
1700 name: &str,
1701 ) -> Result<crate::NativeCollection, UiContextError> {
1702 let mut runtime = self
1703 .runtime
1704 .try_borrow_mut()
1705 .map_err(|_| UiContextError::Borrowed)?;
1706 Ok(runtime
1707 .native_collections
1708 .read_tracked(&self.component, name)?)
1709 }
1710
1711 pub fn get_native_text_document(
1717 &self,
1718 name: &str,
1719 ) -> Result<crate::NativeTextDocument, UiContextError> {
1720 let mut runtime = self
1721 .runtime
1722 .try_borrow_mut()
1723 .map_err(|_| UiContextError::Borrowed)?;
1724 Ok(runtime
1725 .native_documents
1726 .read_tracked(&self.component, name)?)
1727 }
1728
1729 #[cfg(feature = "charts")]
1737 pub fn get_native_chart_data(
1738 &self,
1739 name: &str,
1740 ) -> Result<crate::NativeChartData, UiContextError> {
1741 self.runtime
1742 .try_borrow()
1743 .map_err(|_| UiContextError::Borrowed)?
1744 .native_chart_data
1745 .get(name)
1746 .cloned()
1747 .ok_or_else(|| UiContextError::UnknownChartData(name.to_owned()))
1748 }
1749
1750 pub fn get_app_store_path(
1756 &self,
1757 store: &str,
1758 field: &str,
1759 path: &UiValuePath,
1760 ) -> Result<UiValue, UiContextError> {
1761 let mut runtime = self
1762 .runtime
1763 .try_borrow_mut()
1764 .map_err(|_| UiContextError::Borrowed)?;
1765 Ok(runtime
1766 .stores
1767 .read_path_tracked(&self.component, &StoreId::app(store), field, path)?)
1768 }
1769
1770 pub fn set_app_store(
1776 &self,
1777 store: &str,
1778 field: &str,
1779 value: Dynamic,
1780 ) -> Result<(), UiContextError> {
1781 self.require_mutation()?;
1782 let value = UiValue::from_dynamic(value)?;
1783 let mut runtime = self
1784 .runtime
1785 .try_borrow_mut()
1786 .map_err(|_| UiContextError::Borrowed)?;
1787 let id = StoreId::app(store);
1788 let sensitive = runtime.stores.is_sensitive(&id, field);
1789 let invalidated = runtime.stores.write(&id, field, value.clone())?;
1790 runtime.dirty.extend(invalidated);
1791 runtime.traces.push(
1792 crate::RuntimeTraceKind::Store,
1793 format!("app:{store}"),
1794 format!("set {field}"),
1795 Some(value),
1796 sensitive,
1797 );
1798 Ok(())
1799 }
1800
1801 pub fn set_app_store_path(
1807 &self,
1808 store: &str,
1809 field: &str,
1810 path: &UiValuePath,
1811 value: Dynamic,
1812 ) -> Result<(), UiContextError> {
1813 self.require_mutation()?;
1814 let value = UiValue::from_dynamic(value)?;
1815 let mut runtime = self
1816 .runtime
1817 .try_borrow_mut()
1818 .map_err(|_| UiContextError::Borrowed)?;
1819 let id = StoreId::app(store);
1820 let sensitive = runtime.stores.is_sensitive(&id, field);
1821 let invalidated = runtime.stores.write_path(&id, field, path, value.clone())?;
1822 runtime.dirty.extend(invalidated);
1823 runtime.traces.push(
1824 crate::RuntimeTraceKind::Store,
1825 format!("app:{store}"),
1826 format!("set nested {field}"),
1827 Some(value),
1828 sensitive,
1829 );
1830 Ok(())
1831 }
1832
1833 pub fn get_window_store(&self, store: &str, field: &str) -> Result<UiValue, UiContextError> {
1840 let window = self.window.as_ref().ok_or(UiContextError::MissingWindow)?;
1841 let mut runtime = self
1842 .runtime
1843 .try_borrow_mut()
1844 .map_err(|_| UiContextError::Borrowed)?;
1845 Ok(runtime
1846 .stores
1847 .read_tracked(&self.component, &StoreId::window(window, store), field)?)
1848 }
1849
1850 pub fn get_window_store_path(
1856 &self,
1857 store: &str,
1858 field: &str,
1859 path: &UiValuePath,
1860 ) -> Result<UiValue, UiContextError> {
1861 let window = self.window.as_ref().ok_or(UiContextError::MissingWindow)?;
1862 let mut runtime = self
1863 .runtime
1864 .try_borrow_mut()
1865 .map_err(|_| UiContextError::Borrowed)?;
1866 Ok(runtime.stores.read_path_tracked(
1867 &self.component,
1868 &StoreId::window(window, store),
1869 field,
1870 path,
1871 )?)
1872 }
1873
1874 pub fn set_window_store(
1880 &self,
1881 store: &str,
1882 field: &str,
1883 value: Dynamic,
1884 ) -> Result<(), UiContextError> {
1885 self.require_mutation()?;
1886 let window = self.window.as_ref().ok_or(UiContextError::MissingWindow)?;
1887 let value = UiValue::from_dynamic(value)?;
1888 let mut runtime = self
1889 .runtime
1890 .try_borrow_mut()
1891 .map_err(|_| UiContextError::Borrowed)?;
1892 let id = StoreId::window(window, store);
1893 let sensitive = runtime.stores.is_sensitive(&id, field);
1894 let invalidated = runtime.stores.write(&id, field, value.clone())?;
1895 runtime.dirty.extend(invalidated);
1896 runtime.traces.push(
1897 crate::RuntimeTraceKind::Store,
1898 format!("window:{window}:{store}"),
1899 format!("set {field}"),
1900 Some(value),
1901 sensitive,
1902 );
1903 Ok(())
1904 }
1905
1906 pub fn set_window_store_path(
1913 &self,
1914 store: &str,
1915 field: &str,
1916 path: &UiValuePath,
1917 value: Dynamic,
1918 ) -> Result<(), UiContextError> {
1919 self.require_mutation()?;
1920 let window = self.window.as_ref().ok_or(UiContextError::MissingWindow)?;
1921 let value = UiValue::from_dynamic(value)?;
1922 let mut runtime = self
1923 .runtime
1924 .try_borrow_mut()
1925 .map_err(|_| UiContextError::Borrowed)?;
1926 let id = StoreId::window(window, store);
1927 let sensitive = runtime.stores.is_sensitive(&id, field);
1928 let invalidated = runtime.stores.write_path(&id, field, path, value.clone())?;
1929 runtime.dirty.extend(invalidated);
1930 runtime.traces.push(
1931 crate::RuntimeTraceKind::Store,
1932 format!("window:{window}:{store}"),
1933 format!("set nested {field}"),
1934 Some(value),
1935 sensitive,
1936 );
1937 Ok(())
1938 }
1939
1940 pub fn emit(&self, event: &str, payload: Dynamic) -> Result<(), UiContextError> {
1946 self.require_mutation()?;
1947 let schema = self
1948 .events
1949 .get(event)
1950 .ok_or_else(|| UiContextError::UnknownEvent(event.to_owned()))?;
1951 schema
1952 .payload
1953 .validate(&payload)
1954 .map_err(UiContextError::InvalidEvent)?;
1955 let payload = UiValue::from_dynamic(payload)?;
1956 let mut runtime = self
1957 .runtime
1958 .try_borrow_mut()
1959 .map_err(|_| UiContextError::Borrowed)?;
1960 runtime.pending_events.push(PendingEvent {
1961 target: self.component.clone(),
1962 event: UiEvent {
1963 name: event.to_owned(),
1964 payload,
1965 },
1966 });
1967 runtime.traces.push(
1968 crate::RuntimeTraceKind::Event,
1969 self.component.to_string(),
1970 format!("emit {event}"),
1971 None,
1972 true,
1973 );
1974 Ok(())
1975 }
1976
1977 pub fn dispatch_action(&self, action: &str, payload: Dynamic) -> Result<(), UiContextError> {
1983 self.require_mutation()?;
1984 let id = ActionId::parse(action)?;
1985 let payload = UiValue::from_dynamic(payload)?;
1986 let mut runtime = self
1987 .runtime
1988 .try_borrow_mut()
1989 .map_err(|_| UiContextError::Borrowed)?;
1990 let invocation = runtime.actions.dispatch(&id, payload)?;
1991 runtime.pending_actions.push(invocation);
1992 runtime.traces.push(
1993 crate::RuntimeTraceKind::Action,
1994 self.component.to_string(),
1995 format!("dispatch {action}"),
1996 None,
1997 true,
1998 );
1999 Ok(())
2000 }
2001
2002 pub fn register_action(&self, action: &str, callback: FnPtr) -> Result<(), UiContextError> {
2008 self.require_mutation()?;
2009 let id = ActionId::parse(action)?;
2010 let callback = self.scoped_callback(callback)?;
2011 self.runtime
2012 .try_borrow_mut()
2013 .map_err(|_| UiContextError::Borrowed)?
2014 .actions
2015 .register_or_replace(id, callback);
2016 Ok(())
2017 }
2018
2019 pub fn set_action_enabled(&self, action: &str, enabled: bool) -> Result<(), UiContextError> {
2025 self.require_mutation()?;
2026 let id = ActionId::parse(action)?;
2027 self.runtime
2028 .try_borrow_mut()
2029 .map_err(|_| UiContextError::Borrowed)?
2030 .actions
2031 .set_enabled(&id, enabled)?;
2032 Ok(())
2033 }
2034
2035 pub fn call_capability(
2041 &self,
2042 capability: &str,
2043 method: &str,
2044 input: Dynamic,
2045 ) -> Result<UiValue, UiContextError> {
2046 self.require_mutation()?;
2047 let id = CapabilityId::parse(capability)?;
2048 let input = UiValue::from_dynamic(input)?;
2049 let mut runtime = self
2050 .runtime
2051 .try_borrow_mut()
2052 .map_err(|_| UiContextError::Borrowed)?;
2053 let output = runtime.capabilities.call(&id, method, input)?;
2054 runtime.traces.push(
2055 crate::RuntimeTraceKind::Capability,
2056 self.component.to_string(),
2057 format!("call {capability}.{method}"),
2058 None,
2059 true,
2060 );
2061 Ok(output)
2062 }
2063
2064 fn read_locale<T>(
2065 &self,
2066 read: impl FnOnce(
2067 &LocaleManager,
2068 Option<&str>,
2069 &ComponentInstancePath,
2070 ) -> Result<T, LocaleError>,
2071 ) -> Result<T, UiContextError> {
2072 let mut runtime = self
2073 .runtime
2074 .try_borrow_mut()
2075 .map_err(|_| UiContextError::Borrowed)?;
2076 runtime
2077 .environment_dependencies
2078 .track_locale(self.window.as_deref(), &self.component);
2079 let locale = runtime
2080 .locale
2081 .as_ref()
2082 .ok_or(UiContextError::LocaleUnavailable)?;
2083 Ok(read(locale, self.window.as_deref(), &self.component)?)
2084 }
2085
2086 pub fn text(&self, key: &str) -> Result<String, UiContextError> {
2092 self.read_locale(|locale, window, component| locale.text(window, Some(component), key))
2093 }
2094
2095 pub fn text_direction(&self) -> Result<String, UiContextError> {
2101 match self
2102 .read_locale(|locale, window, component| locale.direction(window, Some(component)))?
2103 {
2104 TextDirection::LeftToRight => Ok("ltr".to_owned()),
2105 TextDirection::RightToLeft => Ok("rtl".to_owned()),
2106 }
2107 }
2108
2109 pub fn today(&self) -> Result<String, UiContextError> {
2117 Ok(self
2118 .runtime
2119 .try_borrow()
2120 .map_err(|_| UiContextError::Borrowed)?
2121 .calendar_clock
2122 .today()
2123 .to_iso())
2124 }
2125
2126 pub fn format_date(&self, iso_date: &str, style: &str) -> Result<String, UiContextError> {
2132 let style = DateStyle::parse(style)?;
2133 self.read_locale(|locale, window, component| {
2134 locale.format_date(window, Some(component), iso_date, style)
2135 })
2136 }
2137
2138 pub fn format_month_year(&self, iso_date: &str) -> Result<String, UiContextError> {
2144 self.read_locale(|locale, window, component| {
2145 locale.format_month_year(window, Some(component), iso_date)
2146 })
2147 }
2148
2149 pub fn calendar_metadata(&self) -> Result<Map, UiContextError> {
2155 let calendar = self.read_locale(|locale, window, component| {
2156 locale.calendar(window, Some(component)).cloned()
2157 })?;
2158 let dynamic = rhai::serde::to_dynamic(calendar)
2159 .map_err(|error| LocaleError::Decode(error.to_string()))?;
2160 Ok(dynamic.cast::<Map>())
2161 }
2162
2163 pub fn number_metadata(&self) -> Result<Map, UiContextError> {
2169 let number = self.read_locale(|locale, window, component| {
2170 locale.number(window, Some(component)).cloned()
2171 })?;
2172 let dynamic = rhai::serde::to_dynamic(number)
2173 .map_err(|error| LocaleError::Decode(error.to_string()))?;
2174 Ok(dynamic.cast::<Map>())
2175 }
2176
2177 pub fn format_integer(
2183 &self,
2184 value: INT,
2185 options: NumberFormatOptions,
2186 ) -> Result<String, UiContextError> {
2187 self.read_locale(|locale, window, component| {
2188 locale.format_integer(window, Some(component), value, options)
2189 })
2190 }
2191
2192 pub fn format_number(
2198 &self,
2199 value: FLOAT,
2200 options: NumberFormatOptions,
2201 ) -> Result<String, UiContextError> {
2202 self.read_locale(|locale, window, component| {
2203 locale.format_number(window, Some(component), value, options)
2204 })
2205 }
2206
2207 pub fn set_locale(&self, locale: &str) -> Result<(), UiContextError> {
2213 self.require_mutation()?;
2214 let mut runtime = self
2215 .runtime
2216 .try_borrow_mut()
2217 .map_err(|_| UiContextError::Borrowed)?;
2218 let changed = {
2219 let locales = runtime
2220 .locale
2221 .as_mut()
2222 .ok_or(UiContextError::LocaleUnavailable)?;
2223 let previous = locales.generation();
2224 locales.set_app(locale)?;
2225 locales.generation() != previous
2226 };
2227 if changed {
2228 let invalidated = runtime.environment_dependencies.invalidate_locale_app();
2229 runtime.dirty.extend(invalidated);
2230 runtime.mark_all_windows_repaint();
2231 }
2232 runtime.traces.push(
2233 crate::RuntimeTraceKind::Locale,
2234 self.component.to_string(),
2235 format!("select {locale}"),
2236 None,
2237 false,
2238 );
2239 Ok(())
2240 }
2241
2242 pub fn set_theme(&self, family: &str, variant: &str) -> Result<(), UiContextError> {
2248 self.set_theme_preference(
2249 ThemeTarget::App,
2250 ThemePreference::Fixed {
2251 selection: ThemeSelection::new(family, variant),
2252 },
2253 format!("select {family}/{variant}"),
2254 )
2255 }
2256
2257 pub fn set_window_theme(&self, family: &str, variant: &str) -> Result<(), UiContextError> {
2263 let window = self.window.clone().ok_or(UiContextError::MissingWindow)?;
2264 self.set_theme_preference(
2265 ThemeTarget::Window(window),
2266 ThemePreference::Fixed {
2267 selection: ThemeSelection::new(family, variant),
2268 },
2269 format!("select window {family}/{variant}"),
2270 )
2271 }
2272
2273 pub fn window_id(&self) -> Result<String, UiContextError> {
2279 self.window.clone().ok_or(UiContextError::MissingWindow)
2280 }
2281
2282 pub fn view_id(&self) -> Result<String, UiContextError> {
2289 self.view.clone().ok_or(UiContextError::MissingView)
2290 }
2291
2292 pub fn viewport_class(&self) -> Result<String, UiContextError> {
2298 let window = self.window.as_ref().ok_or(UiContextError::MissingWindow)?;
2299 let mut runtime = self
2300 .runtime
2301 .try_borrow_mut()
2302 .map_err(|_| UiContextError::Borrowed)?;
2303 runtime
2304 .environment_dependencies
2305 .track_viewport(window, &self.component);
2306 Ok(runtime.responsive.class(window).as_str().to_owned())
2307 }
2308
2309 pub fn open_window(&self, spec: ScriptWindowSpec) -> Result<(), UiContextError> {
2315 self.require_mutation()?;
2316 let window = self.window.as_ref().ok_or(UiContextError::MissingWindow)?;
2317 let id = spec.id.clone();
2318 let mut runtime = self
2319 .runtime
2320 .try_borrow_mut()
2321 .map_err(|_| UiContextError::Borrowed)?;
2322 runtime.windows.request_open_from(window, spec)?;
2323 runtime.traces.push(
2324 crate::RuntimeTraceKind::Window,
2325 self.component.to_string(),
2326 format!("open {id}"),
2327 None,
2328 false,
2329 );
2330 Ok(())
2331 }
2332
2333 pub fn focus_window(&self, id: &str) -> Result<(), UiContextError> {
2339 self.require_mutation()?;
2340 let window = self.window.as_ref().ok_or(UiContextError::MissingWindow)?;
2341 let mut runtime = self
2342 .runtime
2343 .try_borrow_mut()
2344 .map_err(|_| UiContextError::Borrowed)?;
2345 runtime.windows.request_focus_from(window, id)?;
2346 runtime.traces.push(
2347 crate::RuntimeTraceKind::Window,
2348 self.component.to_string(),
2349 format!("focus {id}"),
2350 None,
2351 false,
2352 );
2353 Ok(())
2354 }
2355
2356 pub fn close_window(&self, id: &str) -> Result<(), UiContextError> {
2362 self.require_mutation()?;
2363 let window = self.window.as_ref().ok_or(UiContextError::MissingWindow)?;
2364 let mut runtime = self
2365 .runtime
2366 .try_borrow_mut()
2367 .map_err(|_| UiContextError::Borrowed)?;
2368 runtime.windows.request_close_from(window, id)?;
2369 runtime.traces.push(
2370 crate::RuntimeTraceKind::Window,
2371 self.component.to_string(),
2372 format!("close {id}"),
2373 None,
2374 false,
2375 );
2376 Ok(())
2377 }
2378
2379 pub fn set_close_handler(&self, callback: FnPtr) -> Result<(), UiContextError> {
2386 self.require_mutation()?;
2387 let window = self.window.as_ref().ok_or(UiContextError::MissingWindow)?;
2388 let callback = self.scoped_callback(callback)?;
2389 self.runtime
2390 .try_borrow_mut()
2391 .map_err(|_| UiContextError::Borrowed)?
2392 .windows
2393 .set_close_handler(window, Some(callback))?;
2394 Ok(())
2395 }
2396
2397 pub fn clear_close_handler(&self) -> Result<(), UiContextError> {
2403 self.require_mutation()?;
2404 let window = self.window.as_ref().ok_or(UiContextError::MissingWindow)?;
2405 self.runtime
2406 .try_borrow_mut()
2407 .map_err(|_| UiContextError::Borrowed)?
2408 .windows
2409 .set_close_handler(window, None)?;
2410 Ok(())
2411 }
2412
2413 pub fn set_window_locale(&self, locale: &str) -> Result<(), UiContextError> {
2419 self.require_mutation()?;
2420 let window = self.window.clone().ok_or(UiContextError::MissingWindow)?;
2421 let mut runtime = self
2422 .runtime
2423 .try_borrow_mut()
2424 .map_err(|_| UiContextError::Borrowed)?;
2425 let changed = {
2426 let locales = runtime
2427 .locale
2428 .as_mut()
2429 .ok_or(UiContextError::LocaleUnavailable)?;
2430 let previous = locales.generation();
2431 locales.set_window(window.clone(), locale)?;
2432 locales.generation() != previous
2433 };
2434 if changed {
2435 let invalidated = runtime
2436 .environment_dependencies
2437 .invalidate_locale_window(&window);
2438 runtime.dirty.extend(invalidated);
2439 runtime.mark_window_repaint(window.clone());
2440 }
2441 runtime.traces.push(
2442 crate::RuntimeTraceKind::Locale,
2443 self.component.to_string(),
2444 format!("select window {window} locale {locale}"),
2445 None,
2446 false,
2447 );
2448 Ok(())
2449 }
2450
2451 pub fn set_local_locale(&self, locale: &str) -> Result<(), UiContextError> {
2457 self.require_mutation()?;
2458 let mut runtime = self
2459 .runtime
2460 .try_borrow_mut()
2461 .map_err(|_| UiContextError::Borrowed)?;
2462 let changed = {
2463 let locales = runtime
2464 .locale
2465 .as_mut()
2466 .ok_or(UiContextError::LocaleUnavailable)?;
2467 let previous = locales.generation();
2468 locales.set_scope(self.component.clone(), locale)?;
2469 locales.generation() != previous
2470 };
2471 if changed {
2472 let invalidated = runtime
2473 .environment_dependencies
2474 .invalidate_locale_scope(&self.component);
2475 runtime.dirty.extend(invalidated);
2476 if let Some(window) = &self.window {
2477 runtime.mark_window_repaint(window.clone());
2478 }
2479 }
2480 runtime.traces.push(
2481 crate::RuntimeTraceKind::Locale,
2482 self.component.to_string(),
2483 format!("select subtree locale {locale}"),
2484 None,
2485 false,
2486 );
2487 Ok(())
2488 }
2489
2490 pub fn set_local_theme(&self, family: &str, variant: &str) -> Result<(), UiContextError> {
2496 self.set_theme_preference(
2497 ThemeTarget::Local,
2498 ThemePreference::Fixed {
2499 selection: ThemeSelection::new(family, variant),
2500 },
2501 format!("select subtree {family}/{variant}"),
2502 )
2503 }
2504
2505 pub fn set_theme_system(&self, family: &str) -> Result<(), UiContextError> {
2511 self.set_theme_preference(
2512 ThemeTarget::App,
2513 ThemePreference::System {
2514 family: family.to_owned(),
2515 },
2516 format!("follow system family {family}"),
2517 )
2518 }
2519
2520 pub fn set_window_theme_system(&self, family: &str) -> Result<(), UiContextError> {
2526 let window = self.window.clone().ok_or(UiContextError::MissingWindow)?;
2527 self.set_theme_preference(
2528 ThemeTarget::Window(window),
2529 ThemePreference::System {
2530 family: family.to_owned(),
2531 },
2532 format!("follow window system family {family}"),
2533 )
2534 }
2535
2536 pub fn set_local_theme_system(&self, family: &str) -> Result<(), UiContextError> {
2542 self.set_theme_preference(
2543 ThemeTarget::Local,
2544 ThemePreference::System {
2545 family: family.to_owned(),
2546 },
2547 format!("follow subtree system family {family}"),
2548 )
2549 }
2550
2551 fn set_theme_preference(
2552 &self,
2553 target: ThemeTarget,
2554 preference: ThemePreference,
2555 message: String,
2556 ) -> Result<(), UiContextError> {
2557 self.require_mutation()?;
2558 let mut runtime = self
2559 .runtime
2560 .try_borrow_mut()
2561 .map_err(|_| UiContextError::Borrowed)?;
2562 let invalidated = match &target {
2563 ThemeTarget::App => runtime.environment_dependencies.invalidate_theme_app(),
2564 ThemeTarget::Window(window) => runtime
2565 .environment_dependencies
2566 .invalidate_theme_window(window),
2567 ThemeTarget::Local => runtime
2568 .environment_dependencies
2569 .invalidate_theme_scope(&self.component),
2570 };
2571 let app_target = matches!(&target, ThemeTarget::App);
2572 let theme = runtime
2573 .theme
2574 .as_mut()
2575 .ok_or(UiContextError::ThemeUnavailable)?;
2576 match target {
2577 ThemeTarget::App => theme.set_app(preference)?,
2578 ThemeTarget::Window(window) => theme.set_window(window, preference)?,
2579 ThemeTarget::Local => theme.set_scope(self.component.clone(), preference)?,
2580 }
2581 if app_target {
2582 runtime.mark_all_windows_dirty();
2583 } else {
2584 runtime.dirty.insert(self.component.clone());
2585 }
2586 runtime.dirty.extend(invalidated);
2587 runtime.traces.push(
2588 crate::RuntimeTraceKind::Theme,
2589 self.component.to_string(),
2590 message,
2591 None,
2592 false,
2593 );
2594 Ok(())
2595 }
2596
2597 pub fn set_reduced_motion(&self, reduced: bool) -> Result<(), UiContextError> {
2604 self.require_mutation()?;
2605 let mut runtime = self
2606 .runtime
2607 .try_borrow_mut()
2608 .map_err(|_| UiContextError::Borrowed)?;
2609 runtime.motions.request_preference(if reduced {
2610 crate::MotionPreference::Reduced
2611 } else {
2612 crate::MotionPreference::Normal
2613 });
2614 let now = runtime.clock.now();
2615 runtime.motion_values = runtime.motions.snapshot(now);
2616 runtime.dirty.insert(self.component.clone());
2617 runtime.traces.push(
2618 crate::RuntimeTraceKind::State,
2619 self.component.to_string(),
2620 format!("reduced motion {reduced}"),
2621 None,
2622 false,
2623 );
2624 Ok(())
2625 }
2626
2627 pub fn motion_handle(&self, name: &str) -> Result<crate::MotionHandle, UiContextError> {
2633 let scope = self.motion_scope();
2634 Ok(self
2635 .runtime
2636 .try_borrow()
2637 .map_err(|_| UiContextError::Borrowed)?
2638 .motions
2639 .timeline_handle_for_owner(
2640 &scope,
2641 &self.component,
2642 self.incarnation,
2643 self.generation,
2644 name,
2645 )?)
2646 }
2647
2648 #[must_use]
2649 pub fn motion_tokens(&self) -> crate::ThemeMotion {
2650 let Ok(mut runtime) = self.runtime.try_borrow_mut() else {
2651 return crate::ThemeMotion::default();
2652 };
2653 runtime
2654 .environment_dependencies
2655 .track_theme(self.window.as_deref(), &self.component);
2656 let appearance = self
2657 .window
2658 .as_deref()
2659 .and_then(|window| runtime.window_appearances.get(window).copied())
2660 .unwrap_or(crate::SystemAppearance::Dark);
2661 runtime
2662 .theme
2663 .as_ref()
2664 .and_then(|theme| {
2665 theme
2666 .resolve(self.window.as_deref(), Some(&self.component), appearance)
2667 .ok()
2668 })
2669 .map_or_else(crate::ThemeMotion::default, |theme| {
2670 theme.variant().tokens.motion.clone()
2671 })
2672 }
2673
2674 pub fn motion_duration(&self, role: &str) -> Result<u64, UiContextError> {
2680 self.motion_tokens()
2681 .durations_ms
2682 .get(role)
2683 .copied()
2684 .ok_or_else(|| UiContextError::UnknownMotionToken {
2685 category: "duration",
2686 role: role.to_owned(),
2687 })
2688 }
2689
2690 pub fn motion_easing(&self, role: &str) -> Result<crate::MotionEasing, UiContextError> {
2696 self.motion_tokens()
2697 .easings
2698 .get(role)
2699 .copied()
2700 .ok_or_else(|| UiContextError::UnknownMotionToken {
2701 category: "easing",
2702 role: role.to_owned(),
2703 })
2704 }
2705
2706 pub fn motion_spring(&self, role: &str) -> Result<crate::ThemeMotionSpring, UiContextError> {
2712 self.motion_tokens()
2713 .springs
2714 .get(role)
2715 .copied()
2716 .ok_or_else(|| UiContextError::UnknownMotionToken {
2717 category: "spring",
2718 role: role.to_owned(),
2719 })
2720 }
2721
2722 pub fn motion_distance(&self, role: &str) -> Result<f64, UiContextError> {
2728 self.motion_tokens()
2729 .distances
2730 .get(role)
2731 .copied()
2732 .ok_or_else(|| UiContextError::UnknownMotionToken {
2733 category: "distance",
2734 role: role.to_owned(),
2735 })
2736 }
2737
2738 pub fn motion_stagger(&self, role: &str) -> Result<u64, UiContextError> {
2744 self.motion_tokens()
2745 .staggers_ms
2746 .get(role)
2747 .copied()
2748 .ok_or_else(|| UiContextError::UnknownMotionToken {
2749 category: "stagger",
2750 role: role.to_owned(),
2751 })
2752 }
2753
2754 #[must_use]
2755 pub fn motion_quality(&self) -> crate::MotionQuality {
2756 self.runtime
2757 .try_borrow()
2758 .map_or(crate::MotionQuality::Low, |runtime| {
2759 runtime.motions.quality()
2760 })
2761 }
2762
2763 pub fn play_motion(&self, handle: &crate::MotionHandle) -> Result<(), UiContextError> {
2769 self.require_mutation()?;
2770 let mut runtime = self
2771 .runtime
2772 .try_borrow_mut()
2773 .map_err(|_| UiContextError::Borrowed)?;
2774 runtime.motions.validate_handle_owner(
2775 handle,
2776 &self.motion_scope(),
2777 &self.component,
2778 self.incarnation,
2779 self.generation,
2780 )?;
2781 let now = runtime.clock.now();
2782 runtime.motions.play_timeline(handle, now)?;
2783 Ok(())
2784 }
2785
2786 pub fn pause_motion(&self, handle: &crate::MotionHandle) -> Result<(), UiContextError> {
2792 self.require_mutation()?;
2793 let mut runtime = self
2794 .runtime
2795 .try_borrow_mut()
2796 .map_err(|_| UiContextError::Borrowed)?;
2797 runtime.motions.validate_handle_owner(
2798 handle,
2799 &self.motion_scope(),
2800 &self.component,
2801 self.incarnation,
2802 self.generation,
2803 )?;
2804 let now = runtime.clock.now();
2805 runtime.motions.pause_timeline(handle, now)?;
2806 Ok(())
2807 }
2808
2809 pub fn seek_motion(
2815 &self,
2816 handle: &crate::MotionHandle,
2817 position_ms: u64,
2818 ) -> Result<(), UiContextError> {
2819 self.require_mutation()?;
2820 let mut runtime = self
2821 .runtime
2822 .try_borrow_mut()
2823 .map_err(|_| UiContextError::Borrowed)?;
2824 runtime.motions.validate_handle_owner(
2825 handle,
2826 &self.motion_scope(),
2827 &self.component,
2828 self.incarnation,
2829 self.generation,
2830 )?;
2831 let now = runtime.clock.now();
2832 runtime.motions.seek_timeline(handle, position_ms, now)?;
2833 runtime.motion_values = runtime.motions.snapshot(now);
2834 Ok(())
2835 }
2836
2837 pub fn restart_motion(&self, handle: &crate::MotionHandle) -> Result<(), UiContextError> {
2843 self.require_mutation()?;
2844 let mut runtime = self
2845 .runtime
2846 .try_borrow_mut()
2847 .map_err(|_| UiContextError::Borrowed)?;
2848 runtime.motions.validate_handle_owner(
2849 handle,
2850 &self.motion_scope(),
2851 &self.component,
2852 self.incarnation,
2853 self.generation,
2854 )?;
2855 let now = runtime.clock.now();
2856 runtime.motions.restart_timeline(handle, now)?;
2857 Ok(())
2858 }
2859
2860 pub fn cancel_motion(&self, handle: &crate::MotionHandle) -> Result<(), UiContextError> {
2866 self.require_mutation()?;
2867 let mut runtime = self
2868 .runtime
2869 .try_borrow_mut()
2870 .map_err(|_| UiContextError::Borrowed)?;
2871 runtime.motions.validate_handle_owner(
2872 handle,
2873 &self.motion_scope(),
2874 &self.component,
2875 self.incarnation,
2876 self.generation,
2877 )?;
2878 runtime.motions.cancel_timeline(handle)?;
2879 Ok(())
2880 }
2881
2882 fn motion_scope(&self) -> String {
2883 match (self.window.as_deref(), self.view.as_deref()) {
2884 (Some(window), Some(view)) => format!("window:{window}/view:{view}/root"),
2885 (Some(window), None) => format!("window:{window}/root"),
2886 (None, Some(view)) => format!("view:{view}/root"),
2887 (None, None) => "root".to_owned(),
2888 }
2889 }
2890
2891 pub fn load_image(&self, asset: &AssetId) -> Result<OpaqueHandle, UiContextError> {
2897 self.require_mutation()?;
2898 Ok(self
2899 .runtime
2900 .try_borrow()
2901 .map_err(|_| UiContextError::Borrowed)?
2902 .assets
2903 .load_image(asset)?
2904 .opaque()
2905 .clone())
2906 }
2907
2908 pub fn start_image_decode(
2914 &self,
2915 asset: &AssetId,
2916 success: FnPtr,
2917 error: FnPtr,
2918 ) -> Result<ImageDecodeHandle, UiContextError> {
2919 self.require_mutation()?;
2920 self.require_generation()?;
2921 let success = self.scoped_callback(success)?;
2922 let error = self.scoped_callback(error)?;
2923 let mut runtime = self
2924 .runtime
2925 .try_borrow_mut()
2926 .map_err(|_| UiContextError::Borrowed)?;
2927 crate::RuntimeBudgets::check(
2928 "image_decodes",
2929 runtime.assets.pending_decode_count().saturating_add(1),
2930 runtime.budgets.image_decodes,
2931 )?;
2932 let handle = runtime.assets.start_image_decode(
2933 asset,
2934 self.async_scope
2935 .clone()
2936 .unwrap_or_else(|| AsyncScope::Component(self.component.clone())),
2937 self.generation,
2938 success,
2939 error,
2940 )?;
2941 runtime.traces.push(
2942 crate::RuntimeTraceKind::Task,
2943 self.component.to_string(),
2944 format!("decode image {}", asset.as_str()),
2945 None,
2946 false,
2947 );
2948 Ok(handle)
2949 }
2950
2951 pub fn cancel_image_decode(&self, handle: ImageDecodeHandle) -> Result<bool, UiContextError> {
2957 self.require_mutation()?;
2958 Ok(self
2959 .runtime
2960 .try_borrow()
2961 .map_err(|_| UiContextError::Borrowed)?
2962 .assets
2963 .cancel_image_decode(handle)?)
2964 }
2965
2966 pub fn cancel_task(&self, handle: TaskHandle) -> Result<bool, UiContextError> {
2972 self.require_mutation()?;
2973 Ok(self
2974 .runtime
2975 .try_borrow_mut()
2976 .map_err(|_| UiContextError::Borrowed)?
2977 .tasks
2978 .cancel(handle))
2979 }
2980
2981 pub fn cancel_subscription(&self, handle: SubscriptionHandle) -> Result<bool, UiContextError> {
2987 self.require_mutation()?;
2988 Ok(self
2989 .runtime
2990 .try_borrow_mut()
2991 .map_err(|_| UiContextError::Borrowed)?
2992 .subscriptions
2993 .cancel(handle))
2994 }
2995
2996 pub fn pause_timeout(&self, key: &str) -> Result<bool, UiContextError> {
3002 self.require_mutation()?;
3003 let id = crate::TimerId::new(self.component.clone(), key)?;
3004 let mut runtime = self
3005 .runtime
3006 .try_borrow_mut()
3007 .map_err(|_| UiContextError::Borrowed)?;
3008 let now = runtime.clock.now();
3009 Ok(runtime.timers.pause(&id, now))
3010 }
3011
3012 pub fn resume_timeout(&self, key: &str) -> Result<bool, UiContextError> {
3018 self.require_mutation()?;
3019 let id = crate::TimerId::new(self.component.clone(), key)?;
3020 let mut runtime = self
3021 .runtime
3022 .try_borrow_mut()
3023 .map_err(|_| UiContextError::Borrowed)?;
3024 let now = runtime.clock.now();
3025 Ok(runtime.timers.resume(&id, now))
3026 }
3027
3028 pub fn cancel_timeout(&self, key: &str) -> Result<bool, UiContextError> {
3034 self.require_mutation()?;
3035 let id = crate::TimerId::new(self.component.clone(), key)?;
3036 Ok(self
3037 .runtime
3038 .try_borrow_mut()
3039 .map_err(|_| UiContextError::Borrowed)?
3040 .timers
3041 .cancel(&id))
3042 }
3043
3044 pub fn start_task(
3050 &self,
3051 capability: &str,
3052 method: &str,
3053 input: Dynamic,
3054 success: FnPtr,
3055 error: FnPtr,
3056 ) -> Result<TaskHandle, UiContextError> {
3057 self.require_mutation()?;
3058 self.require_async_start()?;
3059 self.require_generation()?;
3060 let id = CapabilityId::parse(capability)?;
3061 let input = UiValue::from_dynamic(input)?;
3062 let success = self.scoped_callback(success)?;
3063 let error = self.scoped_callback(error)?;
3064 let mut runtime = self
3065 .runtime
3066 .try_borrow_mut()
3067 .map_err(|_| UiContextError::Borrowed)?;
3068 crate::RuntimeBudgets::check(
3069 "background_tasks",
3070 runtime.tasks.active_count().saturating_add(1),
3071 runtime.budgets.background_tasks,
3072 )?;
3073 let (work, output) = runtime.capabilities.start_task(&id, method, input)?;
3074 let handle = runtime.tasks.spawn_cancellable(
3075 self.async_scope
3076 .clone()
3077 .unwrap_or_else(|| AsyncScope::Component(self.component.clone())),
3078 self.generation,
3079 success,
3080 error,
3081 output,
3082 move |cancellation| work.run(cancellation),
3083 )?;
3084 runtime.traces.push(
3085 crate::RuntimeTraceKind::Task,
3086 self.component.to_string(),
3087 format!("start {capability}.{method}"),
3088 None,
3089 true,
3090 );
3091 Ok(handle)
3092 }
3093
3094 pub fn start_subscription(
3100 &self,
3101 capability: &str,
3102 method: &str,
3103 input: Dynamic,
3104 success: FnPtr,
3105 error: FnPtr,
3106 options: SubscriptionOptions,
3107 ) -> Result<SubscriptionHandle, UiContextError> {
3108 self.require_mutation()?;
3109 self.require_async_start()?;
3110 self.require_generation()?;
3111 if !matches!(self.async_scope, Some(AsyncScope::Effect { .. })) {
3112 return Err(UiContextError::SubscriptionRequiresEffect);
3113 }
3114 let id = CapabilityId::parse(capability)?;
3115 let input = UiValue::from_dynamic(input)?;
3116 let success = self.scoped_callback(success)?;
3117 let error = self.scoped_callback(error)?;
3118 let mut runtime = self
3119 .runtime
3120 .try_borrow_mut()
3121 .map_err(|_| UiContextError::Borrowed)?;
3122 crate::RuntimeBudgets::check(
3123 "subscriptions",
3124 runtime.subscriptions.active_count().saturating_add(1),
3125 runtime.budgets.subscriptions,
3126 )?;
3127 let (work, output) = runtime
3128 .capabilities
3129 .start_subscription(&id, method, input)?;
3130 let registration = SubscriptionRegistration::new(
3131 format!("{capability}.{method}"),
3132 self.async_scope
3133 .clone()
3134 .unwrap_or_else(|| AsyncScope::Component(self.component.clone())),
3135 self.generation,
3136 success,
3137 error,
3138 output,
3139 )
3140 .with_options(options);
3141 let (handle, emitter) = runtime.subscriptions.subscribe(registration);
3142 let closer = emitter.clone();
3143 if let Err(spawn_error) = std::thread::Builder::new()
3144 .name("gpui-rhai-subscription".to_owned())
3145 .spawn(move || run_subscription_work(work, emitter, &closer))
3146 {
3147 let _ = runtime
3148 .subscriptions
3149 .cancel_with_reason(handle, SubscriptionCloseReason::StartupFailed);
3150 return Err(AsyncRuntimeError::Spawn(spawn_error).into());
3151 }
3152 runtime.traces.push(
3153 crate::RuntimeTraceKind::Subscription,
3154 self.component.to_string(),
3155 format!("start {capability}.{method}"),
3156 None,
3157 true,
3158 );
3159 Ok(handle)
3160 }
3161
3162 fn require_mutation(&self) -> Result<(), UiContextError> {
3163 if self.phase.allows_mutation() {
3164 Ok(())
3165 } else {
3166 Err(UiContextError::MutationDuringRender)
3167 }
3168 }
3169
3170 fn require_async_start(&self) -> Result<(), UiContextError> {
3171 if self.phase.allows_async_start() {
3172 Ok(())
3173 } else {
3174 Err(UiContextError::AsyncDuringSuspend)
3175 }
3176 }
3177
3178 fn require_generation(&self) -> Result<(), UiContextError> {
3179 if self.generation == ScriptGeneration::default() {
3180 Err(UiContextError::MissingGeneration)
3181 } else {
3182 Ok(())
3183 }
3184 }
3185}
3186
3187fn run_subscription_work(
3188 work: crate::SubscriptionWork,
3189 emitter: crate::SubscriptionEmitter,
3190 closer: &crate::SubscriptionEmitter,
3191) {
3192 work.run(emitter);
3193 closer.close_with_reason(SubscriptionCloseReason::WorkReturned);
3194}
3195
3196impl CustomType for UiContext {
3197 fn build(mut builder: TypeBuilder<Self>) {
3198 builder
3199 .with_name("UiContext")
3200 .with_fn(
3201 "emit",
3202 |context: &mut Self, event: ImmutableString, payload: Dynamic| {
3203 context
3204 .emit(event.as_str(), payload)
3205 .map_err(|error| Box::new(context_runtime_error(&error)))
3206 },
3207 )
3208 .with_fn(
3209 "component_style",
3210 |context: &mut Self, part: ImmutableString, base: crate::Style| {
3211 context.resolve_component_style(part.as_str(), base)
3212 },
3213 )
3214 .with_fn(
3215 "call_capability",
3216 |context: &mut Self,
3217 capability: ImmutableString,
3218 method: ImmutableString,
3219 input: Dynamic| {
3220 context
3221 .call_capability(capability.as_str(), method.as_str(), input)
3222 .map(UiValue::into_dynamic)
3223 .map_err(|error| Box::new(context_runtime_error(&error)))
3224 },
3225 );
3226 register_state_store_context_methods(&mut builder);
3227 register_native_collection_context_methods(&mut builder);
3228 register_native_document_context_methods(&mut builder);
3229 #[cfg(feature = "charts")]
3230 register_native_chart_data_context_methods(&mut builder);
3231 register_signal_context_methods(&mut builder);
3232 register_element_ref_context_methods(&mut builder);
3233 register_async_context_methods(&mut builder);
3234 register_action_context_methods(&mut builder);
3235 register_locale_context_methods(&mut builder);
3236 register_theme_context_methods(&mut builder);
3237 register_motion_context_methods(&mut builder);
3238 register_asset_context_methods(&mut builder);
3239 register_window_context_methods(&mut builder);
3240 }
3241}
3242
3243fn register_native_collection_context_methods(builder: &mut TypeBuilder<UiContext>) {
3244 builder.with_fn(
3245 "get_native_collection",
3246 |context: &mut UiContext, name: ImmutableString| {
3247 context
3248 .get_native_collection(name.as_str())
3249 .map_err(|error| Box::new(context_runtime_error(&error)))
3250 },
3251 );
3252}
3253
3254fn register_native_document_context_methods(builder: &mut TypeBuilder<UiContext>) {
3255 builder.with_fn(
3256 "get_native_text_document",
3257 |context: &mut UiContext, name: ImmutableString| {
3258 context
3259 .get_native_text_document(name.as_str())
3260 .map_err(|error| Box::new(context_runtime_error(&error)))
3261 },
3262 );
3263}
3264
3265#[cfg(feature = "charts")]
3266fn register_native_chart_data_context_methods(builder: &mut TypeBuilder<UiContext>) {
3267 builder.with_fn(
3268 "get_native_chart_data",
3269 |context: &mut UiContext, name: ImmutableString| {
3270 context
3271 .get_native_chart_data(name.as_str())
3272 .map_err(|error| Box::new(context_runtime_error(&error)))
3273 },
3274 );
3275}
3276
3277fn register_state_store_context_methods(builder: &mut TypeBuilder<UiContext>) {
3278 register_state_context_methods(builder);
3279 register_app_store_context_methods(builder);
3280 register_window_store_context_methods(builder);
3281}
3282
3283fn register_state_context_methods(builder: &mut TypeBuilder<UiContext>) {
3284 builder
3285 .with_fn(
3286 "get_state",
3287 |context: &mut UiContext, field: ImmutableString| {
3288 context
3289 .get_state(field.as_str())
3290 .map(UiValue::into_dynamic)
3291 .map_err(|error| Box::new(context_runtime_error(&error)))
3292 },
3293 )
3294 .with_fn(
3295 "get_state_path",
3296 |context: &mut UiContext, field: ImmutableString, path: Array| {
3297 let path = rhai_value_path(path)?;
3298 context
3299 .get_state_path(field.as_str(), &path)
3300 .map(UiValue::into_dynamic)
3301 .map_err(|error| Box::new(context_runtime_error(&error)))
3302 },
3303 )
3304 .with_fn(
3305 "set_state",
3306 |context: &mut UiContext, field: ImmutableString, value: Dynamic| {
3307 context
3308 .set_state(field.as_str(), value)
3309 .map_err(|error| Box::new(context_runtime_error(&error)))
3310 },
3311 )
3312 .with_fn(
3313 "set_state_path",
3314 |context: &mut UiContext, field: ImmutableString, path: Array, value: Dynamic| {
3315 let path = rhai_value_path(path)?;
3316 context
3317 .set_state_path(field.as_str(), &path, value)
3318 .map_err(|error| Box::new(context_runtime_error(&error)))
3319 },
3320 );
3321}
3322
3323fn register_app_store_context_methods(builder: &mut TypeBuilder<UiContext>) {
3324 builder
3325 .with_fn(
3326 "get_app_store",
3327 |context: &mut UiContext, store: ImmutableString, field: ImmutableString| {
3328 context
3329 .get_app_store(store.as_str(), field.as_str())
3330 .map(UiValue::into_dynamic)
3331 .map_err(|error| Box::new(context_runtime_error(&error)))
3332 },
3333 )
3334 .with_fn(
3335 "get_app_store_path",
3336 |context: &mut UiContext,
3337 store: ImmutableString,
3338 field: ImmutableString,
3339 path: Array| {
3340 let path = rhai_value_path(path)?;
3341 context
3342 .get_app_store_path(store.as_str(), field.as_str(), &path)
3343 .map(UiValue::into_dynamic)
3344 .map_err(|error| Box::new(context_runtime_error(&error)))
3345 },
3346 )
3347 .with_fn(
3348 "set_app_store",
3349 |context: &mut UiContext,
3350 store: ImmutableString,
3351 field: ImmutableString,
3352 value: Dynamic| {
3353 context
3354 .set_app_store(store.as_str(), field.as_str(), value)
3355 .map_err(|error| Box::new(context_runtime_error(&error)))
3356 },
3357 )
3358 .with_fn(
3359 "set_app_store_path",
3360 |context: &mut UiContext,
3361 store: ImmutableString,
3362 field: ImmutableString,
3363 path: Array,
3364 value: Dynamic| {
3365 let path = rhai_value_path(path)?;
3366 context
3367 .set_app_store_path(store.as_str(), field.as_str(), &path, value)
3368 .map_err(|error| Box::new(context_runtime_error(&error)))
3369 },
3370 );
3371}
3372
3373fn register_window_store_context_methods(builder: &mut TypeBuilder<UiContext>) {
3374 builder
3375 .with_fn(
3376 "get_window_store",
3377 |context: &mut UiContext, store: ImmutableString, field: ImmutableString| {
3378 context
3379 .get_window_store(store.as_str(), field.as_str())
3380 .map(UiValue::into_dynamic)
3381 .map_err(|error| Box::new(context_runtime_error(&error)))
3382 },
3383 )
3384 .with_fn(
3385 "get_window_store_path",
3386 |context: &mut UiContext,
3387 store: ImmutableString,
3388 field: ImmutableString,
3389 path: Array| {
3390 let path = rhai_value_path(path)?;
3391 context
3392 .get_window_store_path(store.as_str(), field.as_str(), &path)
3393 .map(UiValue::into_dynamic)
3394 .map_err(|error| Box::new(context_runtime_error(&error)))
3395 },
3396 )
3397 .with_fn(
3398 "set_window_store",
3399 |context: &mut UiContext,
3400 store: ImmutableString,
3401 field: ImmutableString,
3402 value: Dynamic| {
3403 context
3404 .set_window_store(store.as_str(), field.as_str(), value)
3405 .map_err(|error| Box::new(context_runtime_error(&error)))
3406 },
3407 )
3408 .with_fn(
3409 "set_window_store_path",
3410 |context: &mut UiContext,
3411 store: ImmutableString,
3412 field: ImmutableString,
3413 path: Array,
3414 value: Dynamic| {
3415 let path = rhai_value_path(path)?;
3416 context
3417 .set_window_store_path(store.as_str(), field.as_str(), &path, value)
3418 .map_err(|error| Box::new(context_runtime_error(&error)))
3419 },
3420 );
3421}
3422
3423fn rhai_value_path(values: Array) -> Result<UiValuePath, Box<EvalAltResult>> {
3424 if values.len() > 64 {
3425 return Err(Box::new(EvalAltResult::ErrorRuntime(
3426 "value path must have at most 64 segments".into(),
3427 Position::NONE,
3428 )));
3429 }
3430 let segments = values
3431 .into_iter()
3432 .enumerate()
3433 .map(|(index, value)| {
3434 let actual = value.type_name().to_owned();
3435 if value.is::<ImmutableString>() {
3436 return Ok(UiValuePathSegment::Key(
3437 value.cast::<ImmutableString>().to_string(),
3438 ));
3439 }
3440 if value.is::<INT>() {
3441 let index_value = value.cast::<INT>();
3442 return usize::try_from(index_value)
3443 .map(UiValuePathSegment::Index)
3444 .map_err(|_| {
3445 Box::new(EvalAltResult::ErrorRuntime(
3446 format!("value path segment {index} must be a non-negative integer")
3447 .into(),
3448 Position::NONE,
3449 ))
3450 });
3451 }
3452 if let Some(mut selector) = value.try_cast::<Map>() {
3453 let by = selector
3454 .remove("by")
3455 .and_then(Dynamic::try_cast::<ImmutableString>);
3456 let key = selector
3457 .remove("key")
3458 .and_then(Dynamic::try_cast::<ImmutableString>);
3459 if selector.is_empty()
3460 && let (Some(by), Some(key)) = (by, key)
3461 {
3462 return Ok(UiValuePathSegment::Item {
3463 key_field: by.to_string(),
3464 key: key.to_string(),
3465 });
3466 }
3467 return Err(Box::new(EvalAltResult::ErrorRuntime(
3468 format!(
3469 "value path segment {index} keyed selector must be exactly #{{ by: string, key: string }}"
3470 )
3471 .into(),
3472 Position::NONE,
3473 )));
3474 }
3475 Err(Box::new(EvalAltResult::ErrorRuntime(
3476 format!(
3477 "value path segment {index} must be a string, non-negative integer, or keyed selector map; got {actual}"
3478 )
3479 .into(),
3480 Position::NONE,
3481 )))
3482 })
3483 .collect::<Result<Vec<_>, _>>()?;
3484 UiValuePath::new(segments).map_err(|error| {
3485 Box::new(EvalAltResult::ErrorRuntime(
3486 error.to_string().into(),
3487 Position::NONE,
3488 ))
3489 })
3490}
3491
3492fn register_signal_context_methods(builder: &mut TypeBuilder<UiContext>) {
3493 builder
3494 .with_fn(
3495 "get_signal",
3496 |context: &mut UiContext, signal: crate::NativeSignal| {
3497 context
3498 .get_signal(&signal)
3499 .map_err(|error| Box::new(context_runtime_error(&error)))
3500 },
3501 )
3502 .with_fn(
3503 "get_signal",
3504 |context: &mut UiContext, key: ImmutableString| {
3505 context
3506 .get_signal_by_key(key.as_str())
3507 .map_err(|error| Box::new(context_runtime_error(&error)))
3508 },
3509 )
3510 .with_fn(
3511 "set_signal",
3512 |context: &mut UiContext, signal: crate::NativeSignal, value: Dynamic| {
3513 context
3514 .set_signal(&signal, value)
3515 .map_err(|error| Box::new(context_runtime_error(&error)))
3516 },
3517 )
3518 .with_fn(
3519 "set_signal",
3520 |context: &mut UiContext, key: ImmutableString, value: Dynamic| {
3521 context
3522 .set_signal_by_key(key.as_str(), value)
3523 .map_err(|error| Box::new(context_runtime_error(&error)))
3524 },
3525 );
3526}
3527
3528fn register_element_ref_context_methods(builder: &mut TypeBuilder<UiContext>) {
3529 builder
3530 .with_fn(
3531 "element_bounds",
3532 |context: &mut UiContext, reference: crate::ElementRef| {
3533 context
3534 .element_bounds(&reference)
3535 .map(UiValue::into_dynamic)
3536 .map_err(|error| Box::new(context_runtime_error(&error)))
3537 },
3538 )
3539 .with_fn(
3540 "element_bounds",
3541 |context: &mut UiContext, key: ImmutableString| {
3542 context
3543 .element_bounds_by_key(key.as_str())
3544 .map(UiValue::into_dynamic)
3545 .map_err(|error| Box::new(context_runtime_error(&error)))
3546 },
3547 )
3548 .with_fn("event_target_bounds", |context: &mut UiContext| {
3549 context
3550 .event_target_bounds()
3551 .map(UiValue::into_dynamic)
3552 .map_err(|error| Box::new(context_runtime_error(&error)))
3553 })
3554 .with_fn(
3555 "focus",
3556 |context: &mut UiContext, reference: crate::ElementRef| {
3557 context
3558 .focus_element(&reference)
3559 .map_err(|error| Box::new(context_runtime_error(&error)))
3560 },
3561 )
3562 .with_fn("focus", |context: &mut UiContext, key: ImmutableString| {
3563 context
3564 .focus_element_by_key(key.as_str())
3565 .map_err(|error| Box::new(context_runtime_error(&error)))
3566 });
3567}
3568
3569fn geometry_bounds_value(bounds: crate::GeometryBounds) -> UiValue {
3570 bounds.into_value()
3571}
3572
3573fn register_action_context_methods(builder: &mut TypeBuilder<UiContext>) {
3574 builder
3575 .with_fn(
3576 "dispatch_action",
3577 |context: &mut UiContext, action: ImmutableString, payload: Dynamic| {
3578 context
3579 .dispatch_action(action.as_str(), payload)
3580 .map_err(|error| Box::new(context_runtime_error(&error)))
3581 },
3582 )
3583 .with_fn(
3584 "register_action",
3585 |call: NativeCallContext<'_>,
3586 context: &mut UiContext,
3587 action: ImmutableString,
3588 callback: FnPtr| {
3589 context_at_call(context, &call)
3590 .register_action(action.as_str(), callback)
3591 .map_err(|error| Box::new(context_runtime_error(&error)))
3592 },
3593 )
3594 .with_fn(
3595 "set_action_enabled",
3596 |context: &mut UiContext, action: ImmutableString, enabled: bool| {
3597 context
3598 .set_action_enabled(action.as_str(), enabled)
3599 .map_err(|error| Box::new(context_runtime_error(&error)))
3600 },
3601 )
3602 .with_fn(
3603 "scroll_to",
3604 |context: &mut UiContext, reference: crate::ElementRef, x: FLOAT, y: FLOAT| {
3605 context
3606 .scroll_element_to(&reference, x, y)
3607 .map_err(|error| Box::new(context_runtime_error(&error)))
3608 },
3609 )
3610 .with_fn(
3611 "scroll_to",
3612 |context: &mut UiContext, key: ImmutableString, x: FLOAT, y: FLOAT| {
3613 context
3614 .scroll_element_to_by_key(key.as_str(), x, y)
3615 .map_err(|error| Box::new(context_runtime_error(&error)))
3616 },
3617 )
3618 .with_fn(
3619 "scroll_into_view",
3620 |context: &mut UiContext, reference: crate::ElementRef| {
3621 context
3622 .scroll_element_into_view(&reference)
3623 .map_err(|error| Box::new(context_runtime_error(&error)))
3624 },
3625 )
3626 .with_fn(
3627 "scroll_into_view",
3628 |context: &mut UiContext, key: ImmutableString| {
3629 context
3630 .scroll_element_into_view_by_key(key.as_str())
3631 .map_err(|error| Box::new(context_runtime_error(&error)))
3632 },
3633 );
3634}
3635
3636fn register_async_context_methods(builder: &mut TypeBuilder<UiContext>) {
3637 builder
3638 .with_fn(
3639 "start_task",
3640 |call: NativeCallContext<'_>,
3641 context: &mut UiContext,
3642 capability: ImmutableString,
3643 method: ImmutableString,
3644 input: Dynamic,
3645 success: FnPtr,
3646 error: FnPtr| {
3647 context_at_call(context, &call)
3648 .start_task(capability.as_str(), method.as_str(), input, success, error)
3649 .map_err(|error| Box::new(context_runtime_error(&error)))
3650 },
3651 )
3652 .with_fn(
3653 "start_subscription",
3654 |call: NativeCallContext<'_>,
3655 context: &mut UiContext,
3656 capability: ImmutableString,
3657 method: ImmutableString,
3658 input: Dynamic,
3659 success: FnPtr,
3660 error: FnPtr,
3661 options: Map| {
3662 let options = subscription_options(options)?;
3663 context_at_call(context, &call)
3664 .start_subscription(
3665 capability.as_str(),
3666 method.as_str(),
3667 input,
3668 success,
3669 error,
3670 options,
3671 )
3672 .map_err(|error| Box::new(context_runtime_error(&error)))
3673 },
3674 )
3675 .with_fn(
3676 "cancel_task",
3677 |context: &mut UiContext, handle: TaskHandle| {
3678 context
3679 .cancel_task(handle)
3680 .map_err(|error| Box::new(context_runtime_error(&error)))
3681 },
3682 )
3683 .with_fn(
3684 "cancel_subscription",
3685 |context: &mut UiContext, handle: SubscriptionHandle| {
3686 context
3687 .cancel_subscription(handle)
3688 .map_err(|error| Box::new(context_runtime_error(&error)))
3689 },
3690 )
3691 .with_fn(
3692 "pause_timeout",
3693 |context: &mut UiContext, key: ImmutableString| {
3694 context
3695 .pause_timeout(key.as_str())
3696 .map_err(|error| Box::new(context_runtime_error(&error)))
3697 },
3698 )
3699 .with_fn(
3700 "resume_timeout",
3701 |context: &mut UiContext, key: ImmutableString| {
3702 context
3703 .resume_timeout(key.as_str())
3704 .map_err(|error| Box::new(context_runtime_error(&error)))
3705 },
3706 )
3707 .with_fn(
3708 "cancel_timeout",
3709 |context: &mut UiContext, key: ImmutableString| {
3710 context
3711 .cancel_timeout(key.as_str())
3712 .map_err(|error| Box::new(context_runtime_error(&error)))
3713 },
3714 );
3715}
3716
3717fn subscription_options(mut options: Map) -> Result<SubscriptionOptions, Box<EvalAltResult>> {
3718 let defaults = SubscriptionOptions::default();
3719 let delivery = options.remove("delivery").map_or(
3720 Ok(defaults.delivery()),
3721 |value| -> Result<_, Box<EvalAltResult>> {
3722 let value = value.try_cast::<ImmutableString>().ok_or_else(|| {
3723 Box::new(EvalAltResult::ErrorRuntime(
3724 "subscription delivery must be `all` or `latest`".into(),
3725 Position::NONE,
3726 ))
3727 })?;
3728 match value.as_str() {
3729 "all" => Ok(SubscriptionDeliveryPolicy::All),
3730 "latest" => Ok(SubscriptionDeliveryPolicy::Latest),
3731 _ => Err(Box::new(EvalAltResult::ErrorRuntime(
3732 "subscription delivery must be `all` or `latest`".into(),
3733 Position::NONE,
3734 ))),
3735 }
3736 },
3737 )?;
3738 let capacity = options.remove("capacity").map_or(
3739 Ok(defaults.capacity()),
3740 |value| -> Result<_, Box<EvalAltResult>> {
3741 value
3742 .try_cast::<INT>()
3743 .and_then(|value| usize::try_from(value).ok())
3744 .ok_or_else(|| {
3745 Box::new(EvalAltResult::ErrorRuntime(
3746 "subscription capacity must be a positive integer".into(),
3747 Position::NONE,
3748 ))
3749 })
3750 },
3751 )?;
3752 let throttle = options.remove("throttle_ms").map_or(
3753 Ok(defaults.throttle()),
3754 |value| -> Result<_, Box<EvalAltResult>> {
3755 value
3756 .try_cast::<INT>()
3757 .and_then(|value| u64::try_from(value).ok())
3758 .map(Duration::from_millis)
3759 .ok_or_else(|| {
3760 Box::new(EvalAltResult::ErrorRuntime(
3761 "subscription throttle_ms must be a non-negative integer".into(),
3762 Position::NONE,
3763 ))
3764 })
3765 },
3766 )?;
3767 if let Some(name) = options.keys().next() {
3768 return Err(Box::new(EvalAltResult::ErrorRuntime(
3769 format!("unknown subscription option `{name}`").into(),
3770 Position::NONE,
3771 )));
3772 }
3773 SubscriptionOptions::new(delivery, capacity, throttle).map_err(|error| {
3774 Box::new(EvalAltResult::ErrorRuntime(
3775 error.to_string().into(),
3776 Position::NONE,
3777 ))
3778 })
3779}
3780
3781fn register_locale_context_methods(builder: &mut TypeBuilder<UiContext>) {
3782 builder
3783 .with_fn("t", |context: &mut UiContext, key: ImmutableString| {
3784 context
3785 .text(key.as_str())
3786 .map_err(|error| Box::new(context_runtime_error(&error)))
3787 })
3788 .with_fn(
3789 "set_locale",
3790 |context: &mut UiContext, locale: ImmutableString| {
3791 context
3792 .set_locale(locale.as_str())
3793 .map_err(|error| Box::new(context_runtime_error(&error)))
3794 },
3795 )
3796 .with_fn("text_direction", |context: &mut UiContext| {
3797 context
3798 .text_direction()
3799 .map_err(|error| Box::new(context_runtime_error(&error)))
3800 })
3801 .with_fn("today", |context: &mut UiContext| {
3802 context
3803 .today()
3804 .map_err(|error| Box::new(context_runtime_error(&error)))
3805 })
3806 .with_fn(
3807 "format_date",
3808 |context: &mut UiContext, date: ImmutableString, style: ImmutableString| {
3809 context
3810 .format_date(date.as_str(), style.as_str())
3811 .map_err(|error| Box::new(context_runtime_error(&error)))
3812 },
3813 )
3814 .with_fn(
3815 "format_month_year",
3816 |context: &mut UiContext, date: ImmutableString| {
3817 context
3818 .format_month_year(date.as_str())
3819 .map_err(|error| Box::new(context_runtime_error(&error)))
3820 },
3821 )
3822 .with_fn("calendar", |context: &mut UiContext| {
3823 context
3824 .calendar_metadata()
3825 .map_err(|error| Box::new(context_runtime_error(&error)))
3826 })
3827 .with_fn("number", |context: &mut UiContext| {
3828 context
3829 .number_metadata()
3830 .map_err(|error| Box::new(context_runtime_error(&error)))
3831 })
3832 .with_fn("format_number", |context: &mut UiContext, value: INT| {
3833 context
3834 .format_integer(value, NumberFormatOptions::default())
3835 .map_err(|error| Box::new(context_runtime_error(&error)))
3836 })
3837 .with_fn("format_number", |context: &mut UiContext, value: FLOAT| {
3838 context
3839 .format_number(value, NumberFormatOptions::default())
3840 .map_err(|error| Box::new(context_runtime_error(&error)))
3841 })
3842 .with_fn(
3843 "format_number",
3844 |context: &mut UiContext,
3845 value: INT,
3846 options: Map|
3847 -> Result<String, Box<EvalAltResult>> {
3848 let options = number_format_options(options)?;
3849 context
3850 .format_integer(value, options)
3851 .map_err(|error| Box::new(context_runtime_error(&error)))
3852 },
3853 )
3854 .with_fn(
3855 "format_number",
3856 |context: &mut UiContext,
3857 value: FLOAT,
3858 options: Map|
3859 -> Result<String, Box<EvalAltResult>> {
3860 let options = number_format_options(options)?;
3861 context
3862 .format_number(value, options)
3863 .map_err(|error| Box::new(context_runtime_error(&error)))
3864 },
3865 );
3866}
3867
3868fn number_format_options(mut options: Map) -> Result<NumberFormatOptions, Box<EvalAltResult>> {
3869 let mut parsed = NumberFormatOptions::default();
3870 if let Some(value) = options.remove("min_fraction_digits") {
3871 let value = value.try_cast::<INT>().ok_or_else(|| {
3872 Box::new(EvalAltResult::ErrorRuntime(
3873 "min_fraction_digits must be an integer".into(),
3874 Position::NONE,
3875 ))
3876 })?;
3877 parsed.min_fraction_digits = u8::try_from(value).map_err(|_| {
3878 Box::new(EvalAltResult::ErrorRuntime(
3879 "min_fraction_digits must be between 0 and 12".into(),
3880 Position::NONE,
3881 ))
3882 })?;
3883 }
3884 if let Some(value) = options.remove("max_fraction_digits") {
3885 let value = value.try_cast::<INT>().ok_or_else(|| {
3886 Box::new(EvalAltResult::ErrorRuntime(
3887 "max_fraction_digits must be an integer".into(),
3888 Position::NONE,
3889 ))
3890 })?;
3891 parsed.max_fraction_digits = u8::try_from(value).map_err(|_| {
3892 Box::new(EvalAltResult::ErrorRuntime(
3893 "max_fraction_digits must be between 0 and 12".into(),
3894 Position::NONE,
3895 ))
3896 })?;
3897 }
3898 if let Some(value) = options.remove("grouping") {
3899 parsed.grouping = value.try_cast::<bool>().ok_or_else(|| {
3900 Box::new(EvalAltResult::ErrorRuntime(
3901 "grouping must be a bool".into(),
3902 Position::NONE,
3903 ))
3904 })?;
3905 }
3906 if let Some((unknown, _)) = options.into_iter().next() {
3907 return Err(Box::new(EvalAltResult::ErrorRuntime(
3908 format!("unknown number format option `{unknown}`").into(),
3909 Position::NONE,
3910 )));
3911 }
3912 parsed.validate().map_err(|error| {
3913 Box::new(EvalAltResult::ErrorRuntime(
3914 error.to_string().into(),
3915 Position::NONE,
3916 ))
3917 })?;
3918 Ok(parsed)
3919}
3920
3921fn register_theme_context_methods(builder: &mut TypeBuilder<UiContext>) {
3922 builder
3923 .with_fn(
3924 "set_theme",
3925 |context: &mut UiContext, family: ImmutableString, variant: ImmutableString| {
3926 context
3927 .set_theme(family.as_str(), variant.as_str())
3928 .map_err(|error| Box::new(context_runtime_error(&error)))
3929 },
3930 )
3931 .with_fn(
3932 "set_window_theme",
3933 |context: &mut UiContext, family: ImmutableString, variant: ImmutableString| {
3934 context
3935 .set_window_theme(family.as_str(), variant.as_str())
3936 .map_err(|error| Box::new(context_runtime_error(&error)))
3937 },
3938 )
3939 .with_fn(
3940 "set_local_theme",
3941 |context: &mut UiContext, family: ImmutableString, variant: ImmutableString| {
3942 context
3943 .set_local_theme(family.as_str(), variant.as_str())
3944 .map_err(|error| Box::new(context_runtime_error(&error)))
3945 },
3946 )
3947 .with_fn(
3948 "set_theme_system",
3949 |context: &mut UiContext, family: ImmutableString| {
3950 context
3951 .set_theme_system(family.as_str())
3952 .map_err(|error| Box::new(context_runtime_error(&error)))
3953 },
3954 )
3955 .with_fn(
3956 "set_window_theme_system",
3957 |context: &mut UiContext, family: ImmutableString| {
3958 context
3959 .set_window_theme_system(family.as_str())
3960 .map_err(|error| Box::new(context_runtime_error(&error)))
3961 },
3962 )
3963 .with_fn(
3964 "set_local_theme_system",
3965 |context: &mut UiContext, family: ImmutableString| {
3966 context
3967 .set_local_theme_system(family.as_str())
3968 .map_err(|error| Box::new(context_runtime_error(&error)))
3969 },
3970 );
3971}
3972
3973#[allow(clippy::too_many_lines)]
3974fn register_motion_context_methods(builder: &mut TypeBuilder<UiContext>) {
3975 builder
3976 .with_fn(
3977 "set_reduced_motion",
3978 |context: &mut UiContext, reduced: bool| {
3979 context
3980 .set_reduced_motion(reduced)
3981 .map_err(|error| Box::new(context_runtime_error(&error)))
3982 },
3983 )
3984 .with_fn(
3985 "motion_handle",
3986 |context: &mut UiContext, name: ImmutableString| {
3987 context
3988 .motion_handle(name.as_str())
3989 .map_err(|error| Box::new(context_runtime_error(&error)))
3990 },
3991 )
3992 .with_fn(
3993 "motion_duration",
3994 |context: &mut UiContext, role: ImmutableString| {
3995 context
3996 .motion_duration(role.as_str())
3997 .map(|value| INT::try_from(value).unwrap_or(INT::MAX))
3998 .map_err(|error| Box::new(context_runtime_error(&error)))
3999 },
4000 )
4001 .with_fn(
4002 "motion_easing",
4003 |context: &mut UiContext, role: ImmutableString| {
4004 context
4005 .motion_easing(role.as_str())
4006 .map(|value| match value {
4007 crate::MotionEasing::Linear => "linear",
4008 crate::MotionEasing::EaseIn => "ease_in",
4009 crate::MotionEasing::EaseOut => "ease_out",
4010 crate::MotionEasing::EaseInOut => "ease_in_out",
4011 })
4012 .map(ImmutableString::from)
4013 .map_err(|error| Box::new(context_runtime_error(&error)))
4014 },
4015 )
4016 .with_fn(
4017 "motion_spring",
4018 |context: &mut UiContext, role: ImmutableString| {
4019 context
4020 .motion_spring(role.as_str())
4021 .map(|spring| {
4022 Map::from_iter([
4023 ("stiffness".into(), Dynamic::from_float(spring.stiffness)),
4024 ("damping".into(), Dynamic::from_float(spring.damping)),
4025 ("mass".into(), Dynamic::from_float(spring.mass)),
4026 ])
4027 })
4028 .map_err(|error| Box::new(context_runtime_error(&error)))
4029 },
4030 )
4031 .with_fn(
4032 "motion_distance",
4033 |context: &mut UiContext, role: ImmutableString| {
4034 context
4035 .motion_distance(role.as_str())
4036 .map_err(|error| Box::new(context_runtime_error(&error)))
4037 },
4038 )
4039 .with_fn(
4040 "motion_stagger",
4041 |context: &mut UiContext, role: ImmutableString| {
4042 context
4043 .motion_stagger(role.as_str())
4044 .map(|value| INT::try_from(value).unwrap_or(INT::MAX))
4045 .map_err(|error| Box::new(context_runtime_error(&error)))
4046 },
4047 )
4048 .with_fn("motion_quality", |context: &mut UiContext| {
4049 match context.motion_quality() {
4050 crate::MotionQuality::Low => "low",
4051 crate::MotionQuality::Medium => "medium",
4052 crate::MotionQuality::High => "high",
4053 }
4054 })
4055 .with_fn(
4056 "play_motion",
4057 |context: &mut UiContext, handle: crate::MotionHandle| {
4058 context
4059 .play_motion(&handle)
4060 .map_err(|error| Box::new(context_runtime_error(&error)))
4061 },
4062 )
4063 .with_fn(
4064 "resume_motion",
4065 |context: &mut UiContext, handle: crate::MotionHandle| {
4066 context
4067 .play_motion(&handle)
4068 .map_err(|error| Box::new(context_runtime_error(&error)))
4069 },
4070 )
4071 .with_fn(
4072 "pause_motion",
4073 |context: &mut UiContext, handle: crate::MotionHandle| {
4074 context
4075 .pause_motion(&handle)
4076 .map_err(|error| Box::new(context_runtime_error(&error)))
4077 },
4078 )
4079 .with_fn(
4080 "seek_motion",
4081 |context: &mut UiContext, handle: crate::MotionHandle, position_ms: INT| {
4082 let position_ms = u64::try_from(position_ms).map_err(|_| {
4083 Box::new(context_runtime_error(&UiContextError::Motion(
4084 crate::MotionError::InvalidTimeline(
4085 "seek position must be non-negative".to_owned(),
4086 ),
4087 )))
4088 })?;
4089 context
4090 .seek_motion(&handle, position_ms)
4091 .map_err(|error| Box::new(context_runtime_error(&error)))
4092 },
4093 )
4094 .with_fn(
4095 "restart_motion",
4096 |context: &mut UiContext, handle: crate::MotionHandle| {
4097 context
4098 .restart_motion(&handle)
4099 .map_err(|error| Box::new(context_runtime_error(&error)))
4100 },
4101 )
4102 .with_fn(
4103 "cancel_motion",
4104 |context: &mut UiContext, handle: crate::MotionHandle| {
4105 context
4106 .cancel_motion(&handle)
4107 .map_err(|error| Box::new(context_runtime_error(&error)))
4108 },
4109 );
4110}
4111
4112fn register_asset_context_methods(builder: &mut TypeBuilder<UiContext>) {
4113 builder
4114 .with_fn("load_image", |context: &mut UiContext, asset: AssetId| {
4115 context
4116 .load_image(&asset)
4117 .map_err(|error| Box::new(context_runtime_error(&error)))
4118 })
4119 .with_fn(
4120 "start_image_decode",
4121 |call: NativeCallContext<'_>,
4122 context: &mut UiContext,
4123 asset: AssetId,
4124 success: FnPtr,
4125 error: FnPtr| {
4126 context_at_call(context, &call)
4127 .start_image_decode(&asset, success, error)
4128 .map_err(|error| Box::new(context_runtime_error(&error)))
4129 },
4130 )
4131 .with_fn(
4132 "cancel_image_decode",
4133 |context: &mut UiContext, handle: ImageDecodeHandle| {
4134 context
4135 .cancel_image_decode(handle)
4136 .map_err(|error| Box::new(context_runtime_error(&error)))
4137 },
4138 );
4139}
4140
4141fn register_window_context_methods(builder: &mut TypeBuilder<UiContext>) {
4142 builder
4143 .with_fn("window_id", |context: &mut UiContext| {
4144 context
4145 .window_id()
4146 .map_err(|error| Box::new(context_runtime_error(&error)))
4147 })
4148 .with_fn("view_id", |context: &mut UiContext| {
4149 context
4150 .view_id()
4151 .map_err(|error| Box::new(context_runtime_error(&error)))
4152 })
4153 .with_fn("viewport_class", |context: &mut UiContext| {
4154 context
4155 .viewport_class()
4156 .map_err(|error| Box::new(context_runtime_error(&error)))
4157 })
4158 .with_fn(
4159 "open_window",
4160 |context: &mut UiContext,
4161 id: ImmutableString,
4162 title: ImmutableString,
4163 width: rhai::INT,
4164 height: rhai::INT,
4165 focus: bool| {
4166 context
4167 .open_window(ScriptWindowSpec {
4168 id: id.to_string(),
4169 title: title.to_string(),
4170 width: width.to_string().parse::<f64>().unwrap_or(f64::INFINITY),
4171 height: height.to_string().parse::<f64>().unwrap_or(f64::INFINITY),
4172 focus,
4173 })
4174 .map_err(|error| Box::new(context_runtime_error(&error)))
4175 },
4176 )
4177 .with_fn(
4178 "focus_window",
4179 |context: &mut UiContext, id: ImmutableString| {
4180 context
4181 .focus_window(id.as_str())
4182 .map_err(|error| Box::new(context_runtime_error(&error)))
4183 },
4184 )
4185 .with_fn(
4186 "close_window",
4187 |context: &mut UiContext, id: ImmutableString| {
4188 context
4189 .close_window(id.as_str())
4190 .map_err(|error| Box::new(context_runtime_error(&error)))
4191 },
4192 )
4193 .with_fn(
4194 "set_close_handler",
4195 |call: NativeCallContext<'_>, context: &mut UiContext, callback: FnPtr| {
4196 context_at_call(context, &call)
4197 .set_close_handler(callback)
4198 .map_err(|error| Box::new(context_runtime_error(&error)))
4199 },
4200 )
4201 .with_fn("clear_close_handler", |context: &mut UiContext| {
4202 context
4203 .clear_close_handler()
4204 .map_err(|error| Box::new(context_runtime_error(&error)))
4205 })
4206 .with_fn(
4207 "set_window_locale",
4208 |context: &mut UiContext, locale: ImmutableString| {
4209 context
4210 .set_window_locale(locale.as_str())
4211 .map_err(|error| Box::new(context_runtime_error(&error)))
4212 },
4213 )
4214 .with_fn(
4215 "set_local_locale",
4216 |context: &mut UiContext, locale: ImmutableString| {
4217 context
4218 .set_local_locale(locale.as_str())
4219 .map_err(|error| Box::new(context_runtime_error(&error)))
4220 },
4221 );
4222}
4223
4224fn context_at_call(context: &UiContext, call: &NativeCallContext<'_>) -> UiContext {
4225 context
4226 .clone()
4227 .with_native_context(Some(crate::invocation::ScriptInvocationContext::capture(
4228 call,
4229 )))
4230}
4231
4232pub(crate) fn register_ui_context_api(engine: &mut Engine) {
4233 engine.build_type::<UiContext>();
4234}
4235
4236fn context_runtime_error(error: &UiContextError) -> EvalAltResult {
4237 EvalAltResult::ErrorRuntime(error.to_string().into(), Position::NONE)
4238}
4239
4240#[derive(Debug, Error)]
4241pub enum UiContextError {
4242 #[error("UI runtime state is already mutably borrowed by another callback")]
4243 Borrowed,
4244 #[error("state mutation and effects are forbidden during view rendering")]
4245 MutationDuringRender,
4246 #[error("new async work is forbidden while a view is suspending")]
4247 AsyncDuringSuspend,
4248 #[error("subscriptions must be started by a declarative component effect")]
4249 SubscriptionRequiresEffect,
4250 #[error("event target geometry is available only during event callbacks")]
4251 EventTargetOutsideEvent,
4252 #[error("async work requires a bound script generation")]
4253 MissingGeneration,
4254 #[error("no locale manager is configured for this application")]
4255 LocaleUnavailable,
4256 #[error("no theme manager is configured for this application")]
4257 ThemeUnavailable,
4258 #[cfg(feature = "charts")]
4259 #[error("native chart data name `{0}` must be a safe identifier")]
4260 InvalidChartDataName(String),
4261 #[cfg(feature = "charts")]
4262 #[error("native chart data `{0}` is not registered")]
4263 UnknownChartData(String),
4264 #[error("unknown theme motion {category} token `{role}`")]
4265 UnknownMotionToken {
4266 category: &'static str,
4267 role: String,
4268 },
4269 #[error("component `{component}` has no state field `{field}`")]
4270 UnknownState {
4271 component: ComponentInstancePath,
4272 field: String,
4273 },
4274 #[error("event `{0}` is not declared by this component")]
4275 UnknownEvent(String),
4276 #[error("event payload is invalid: {0}")]
4277 InvalidEvent(crate::SchemaValidationError),
4278 #[error("this UI context is not associated with a window")]
4279 MissingWindow,
4280 #[error("this UI context is not associated with a mounted script view")]
4281 MissingView,
4282 #[error(transparent)]
4283 Window(#[from] WindowCommandError),
4284 #[error(transparent)]
4285 Responsive(#[from] ResponsiveError),
4286 #[error(transparent)]
4287 State(#[from] StateError),
4288 #[error(transparent)]
4289 Store(#[from] StoreError),
4290 #[error(transparent)]
4291 NativeCollection(#[from] crate::NativeCollectionError),
4292 #[error(transparent)]
4293 Document(#[from] crate::DocumentError),
4294 #[error(transparent)]
4295 Action(#[from] ActionError),
4296 #[error(transparent)]
4297 Capability(#[from] CapabilityError),
4298 #[error(transparent)]
4299 Async(#[from] AsyncRuntimeError),
4300 #[error(transparent)]
4301 Locale(#[from] LocaleError),
4302 #[error(transparent)]
4303 Theme(#[from] ThemeError),
4304 #[error(transparent)]
4305 Asset(#[from] AssetError),
4306 #[error(transparent)]
4307 Value(#[from] UiValueError),
4308 #[error(transparent)]
4309 ValuePath(#[from] UiValuePathError),
4310 #[error(transparent)]
4311 Signal(#[from] crate::SignalError),
4312 #[error(transparent)]
4313 ElementRef(#[from] crate::ElementRefError),
4314 #[error(transparent)]
4315 Geometry(#[from] crate::GeometryError),
4316 #[error("scroll offset must be finite and non-negative, got ({x}, {y})")]
4317 InvalidScrollOffset { x: f64, y: f64 },
4318 #[error(transparent)]
4319 Callback(#[from] crate::ScriptCallbackDefinitionError),
4320 #[error(transparent)]
4321 Timer(#[from] crate::TimerError),
4322 #[error(transparent)]
4323 Motion(#[from] crate::MotionError),
4324 #[error(transparent)]
4325 Budget(#[from] crate::RuntimeBudgetError),
4326}
4327
4328#[cfg(test)]
4329mod tests {
4330 use super::*;
4331 use crate::{ComponentStateSchema, RuntimeEngine, StateField, SubscriptionWork, ValueSchema};
4332
4333 fn mounted_context(phase: ExecutionPhase) -> UiContext {
4334 let path = ComponentInstancePath::root("Counter", "counter");
4335 let schema = ComponentStateSchema::new(BTreeMap::from([
4336 (
4337 "count".to_owned(),
4338 StateField::new(ValueSchema::integer(), UiValue::Integer(0)),
4339 ),
4340 (
4341 "profile".to_owned(),
4342 StateField::new(
4343 ValueSchema::UiValue,
4344 UiValue::Map(BTreeMap::from([(
4345 "name".to_owned(),
4346 UiValue::String("Ada".to_owned()),
4347 )])),
4348 ),
4349 ),
4350 ]))
4351 .unwrap();
4352 let mut state = UiRuntimeState::new();
4353 let mut render = state.component_state.begin_render();
4354 render.mount(path.clone(), &schema).unwrap();
4355 state.component_state.commit_render(render);
4356 UiContext::new(
4357 Rc::new(RefCell::new(state)),
4358 path,
4359 Some("main".to_owned()),
4360 phase,
4361 BTreeMap::from([(
4362 "change".to_owned(),
4363 EventSchema {
4364 payload: ValueSchema::integer(),
4365 },
4366 )]),
4367 )
4368 }
4369
4370 #[test]
4371 fn render_signal_reads_disable_component_reuse() {
4372 let context = mounted_context(ExecutionPhase::Render);
4373 let signal = crate::NativeSignal::new(
4374 crate::SignalId::new(
4375 context.component_path().clone(),
4376 "hot",
4377 crate::SignalKind::Float,
4378 )
4379 .unwrap(),
4380 );
4381 assert!(context.get_signal(&signal).is_err());
4382 assert!(!context.component_render_is_reusable());
4383 }
4384
4385 #[test]
4386 fn render_context_rejects_mutation() {
4387 let context = mounted_context(ExecutionPhase::Render);
4388 assert!(matches!(
4389 context.set_state("count", Dynamic::from(1_i64)),
4390 Err(UiContextError::MutationDuringRender)
4391 ));
4392 }
4393
4394 #[test]
4395 fn event_target_bounds_are_untracked_and_event_only() {
4396 let context = mounted_context(ExecutionPhase::Event).with_event_target(Some(
4397 crate::GeometryBounds::new(13.0, 24.0, 90.0, 40.0).unwrap(),
4398 ));
4399 assert_eq!(
4400 context.event_target_bounds().unwrap(),
4401 crate::GeometryBounds::new(13.0, 24.0, 90.0, 40.0)
4402 .unwrap()
4403 .into_value()
4404 );
4405 assert!(context.runtime().borrow().geometry.take_dirty().is_empty());
4406
4407 assert!(matches!(
4408 mounted_context(ExecutionPhase::Render).event_target_bounds(),
4409 Err(UiContextError::EventTargetOutsideEvent)
4410 ));
4411 assert_eq!(
4412 mounted_context(ExecutionPhase::Event)
4413 .event_target_bounds()
4414 .unwrap(),
4415 UiValue::Null
4416 );
4417 }
4418
4419 #[test]
4420 fn viewport_class_is_window_scoped_and_script_readable() {
4421 let context = mounted_context(ExecutionPhase::Render);
4422 context
4423 .runtime()
4424 .borrow_mut()
4425 .responsive
4426 .update_window("main", 480.0)
4427 .unwrap();
4428 assert_eq!(context.viewport_class().unwrap(), "compact");
4429 }
4430
4431 #[test]
4432 fn locale_and_viewport_reads_register_exact_component_dependencies() {
4433 let engine = Engine::new();
4434 let en = crate::load_locale_source(
4435 &engine,
4436 "en.rhai",
4437 include_str!("../../../registry/locales/en.rhai"),
4438 )
4439 .unwrap();
4440 let zh = crate::load_locale_source(
4441 &engine,
4442 "zh_cn.rhai",
4443 include_str!("../../../registry/locales/zh_cn.rhai"),
4444 )
4445 .unwrap();
4446 let mut state = UiRuntimeState::new();
4447 state.locale = Some(LocaleManager::new([en, zh], "en", "en").unwrap());
4448 state.responsive.update_window("main", 480.0).unwrap();
4449 state.windows.register_open("main").unwrap();
4450 let runtime = Rc::new(RefCell::new(state));
4451 let reader = ComponentInstancePath::root("View", "main").child("Reader", "reader");
4452 let unrelated = ComponentInstancePath::root("View", "main").child("Static", "static");
4453 let render_context = UiContext::new(
4454 Rc::clone(&runtime),
4455 reader.clone(),
4456 Some("main".to_owned()),
4457 ExecutionPhase::Render,
4458 BTreeMap::new(),
4459 );
4460 assert!(!render_context.text("common.loading").unwrap().is_empty());
4461 assert_eq!(render_context.viewport_class().unwrap(), "compact");
4462 let event = UiContext::new(
4463 Rc::clone(&runtime),
4464 unrelated,
4465 Some("main".to_owned()),
4466 ExecutionPhase::Event,
4467 BTreeMap::new(),
4468 );
4469 event.set_locale("zh-CN").unwrap();
4470
4471 assert_eq!(
4472 runtime.borrow_mut().drain_batch().dirty,
4473 BTreeSet::from([reader.clone()])
4474 );
4475 assert_eq!(
4476 runtime
4477 .borrow()
4478 .environment_dependencies
4479 .invalidate_viewport("main"),
4480 BTreeSet::from([reader])
4481 );
4482 assert!(runtime.borrow_mut().take_window_repaint("main"));
4483 }
4484
4485 #[test]
4486 fn event_mutations_are_batched() {
4487 let context = mounted_context(ExecutionPhase::Event);
4488 context.set_state("count", Dynamic::from(1_i64)).unwrap();
4489 context.set_state("count", Dynamic::from(2_i64)).unwrap();
4490 context.emit("change", Dynamic::from(2_i64)).unwrap();
4491
4492 let mut runtime = context.runtime().borrow_mut();
4493 let batch = runtime.drain_batch();
4494 assert_eq!(batch.dirty.len(), 1);
4495 assert_eq!(batch.events.len(), 1);
4496 assert_eq!(
4497 runtime
4498 .component_state
4499 .get(&ComponentInstancePath::root("Counter", "counter"), "count",),
4500 Some(&UiValue::Integer(2))
4501 );
4502 }
4503
4504 #[test]
4505 fn semantic_event_and_action_traces_never_retain_payload_values() {
4506 let secret = "AUDIT_ONLY_SYNTHETIC_TOKEN";
4507 let path = ComponentInstancePath::root("SecretInput", "audit");
4508 let schema = ComponentStateSchema::new(BTreeMap::from([(
4509 "token".to_owned(),
4510 StateField::new(ValueSchema::string(), UiValue::String(secret.to_owned()))
4511 .sensitive(true),
4512 )]))
4513 .unwrap();
4514 let mut state = UiRuntimeState::new();
4515 let mut render = state.component_state.begin_render();
4516 render.mount(path.clone(), &schema).unwrap();
4517 state.component_state.commit_render(render);
4518
4519 let mut engine = RuntimeEngine::new();
4520 let compiled = engine
4521 .compile("fn view() { text(\"trace\") } fn receive(ctx, payload) {}")
4522 .unwrap();
4523 state.actions.register_or_replace(
4524 crate::ActionId::parse("audit.receive").unwrap(),
4525 engine.callback(&compiled, "receive").unwrap(),
4526 );
4527 let runtime = Rc::new(RefCell::new(state));
4528 let context = UiContext::new(
4529 Rc::clone(&runtime),
4530 path,
4531 Some("main".to_owned()),
4532 ExecutionPhase::Event,
4533 BTreeMap::from([(
4534 "change".to_owned(),
4535 EventSchema {
4536 payload: ValueSchema::string(),
4537 },
4538 )]),
4539 );
4540
4541 let event_value = context.get_state("token").unwrap().into_dynamic();
4542 context.emit("change", event_value).unwrap();
4543 let action_value = context.get_state("token").unwrap().into_dynamic();
4544 context
4545 .dispatch_action("audit.receive", action_value)
4546 .unwrap();
4547
4548 let traces = runtime.borrow().traces.snapshot();
4549 let semantic = traces
4550 .iter()
4551 .filter(|trace| {
4552 matches!(
4553 trace.kind,
4554 crate::RuntimeTraceKind::Event | crate::RuntimeTraceKind::Action
4555 )
4556 })
4557 .collect::<Vec<_>>();
4558 assert_eq!(semantic.len(), 2);
4559 assert!(
4560 semantic
4561 .iter()
4562 .all(|trace| trace.sensitive && trace.payload.is_none())
4563 );
4564 assert!(!format!("{traces:?}").contains(secret));
4565 }
4566
4567 #[test]
4568 fn scripts_receive_restricted_context_without_gpui_types() {
4569 let context = mounted_context(ExecutionPhase::Event);
4570 let mut runtime = RuntimeEngine::new();
4571 let compiled = runtime
4572 .compile(
4573 r#"
4574 fn view() { text("context") }
4575 fn increment(ctx) {
4576 ctx.set_state("count", ctx.get_state("count") + 1);
4577 }
4578 "#,
4579 )
4580 .unwrap();
4581 runtime.render(&compiled).unwrap();
4582 let callback = runtime.callback(&compiled, "increment").unwrap();
4583 let _ = runtime
4584 .invoke_callback(&compiled, &callback, (context.clone(),))
4585 .unwrap();
4586 assert_eq!(context.get_state("count").unwrap(), UiValue::Integer(1));
4587 }
4588
4589 #[test]
4590 fn scripts_use_bounded_nested_and_keyed_store_paths() {
4591 let context = mounted_context(ExecutionPhase::Event);
4592 let row = |id: &str, label: &str| {
4593 UiValue::Map(BTreeMap::from([
4594 ("id".to_owned(), UiValue::String(id.to_owned())),
4595 ("label".to_owned(), UiValue::String(label.to_owned())),
4596 ]))
4597 };
4598 context
4599 .runtime()
4600 .borrow_mut()
4601 .stores
4602 .declare(
4603 StoreId::app("model"),
4604 ComponentStateSchema::new(BTreeMap::from([(
4605 "data".to_owned(),
4606 StateField::new(
4607 ValueSchema::UiValue,
4608 UiValue::Map(BTreeMap::from([(
4609 "rows".to_owned(),
4610 UiValue::Array(vec![row("alpha", "Alpha"), row("beta", "Beta")]),
4611 )])),
4612 ),
4613 )]))
4614 .unwrap(),
4615 )
4616 .unwrap();
4617 let mut runtime = RuntimeEngine::new();
4618 let compiled = runtime
4619 .compile(
4620 r#"
4621 fn view() { text("paths") }
4622 fn update(ctx) {
4623 let name = ctx.get_state_path("profile", ["name"]);
4624 ctx.set_state_path("profile", ["name"], name + " Lovelace");
4625 let label = ctx.get_app_store_path(
4626 "model",
4627 "data",
4628 ["rows", #{ by: "id", key: "beta" }, "label"]
4629 );
4630 ctx.set_app_store_path(
4631 "model",
4632 "data",
4633 ["rows", #{ by: "id", key: "beta" }, "label"],
4634 label + "!"
4635 );
4636 }
4637 "#,
4638 )
4639 .unwrap();
4640 runtime.render(&compiled).unwrap();
4641 let callback = runtime.callback(&compiled, "update").unwrap();
4642 let _: Dynamic = runtime
4643 .invoke_callback(&compiled, &callback, (context.clone(),))
4644 .unwrap();
4645
4646 let profile = context.get_state("profile").unwrap();
4647 assert_eq!(
4648 profile
4649 .get_path(
4650 &UiValuePath::new(vec![UiValuePathSegment::Key("name".to_owned())]).unwrap()
4651 )
4652 .unwrap(),
4653 &UiValue::String("Ada Lovelace".to_owned())
4654 );
4655 let row_path = UiValuePath::new(vec![
4656 UiValuePathSegment::Key("rows".to_owned()),
4657 UiValuePathSegment::Item {
4658 key_field: "id".to_owned(),
4659 key: "beta".to_owned(),
4660 },
4661 UiValuePathSegment::Key("label".to_owned()),
4662 ])
4663 .unwrap();
4664 assert_eq!(
4665 context
4666 .runtime()
4667 .borrow_mut()
4668 .stores
4669 .read_path_tracked(
4670 &ComponentInstancePath::root("Test", "reader"),
4671 &StoreId::app("model"),
4672 "data",
4673 &row_path,
4674 )
4675 .unwrap(),
4676 UiValue::String("Beta!".to_owned())
4677 );
4678 assert!(rhai_value_path(vec![Dynamic::from(-1_i64)]).is_err());
4679 }
4680
4681 #[test]
4682 fn script_theme_switch_preserves_component_state_and_ast_generation() {
4683 let context = mounted_context(ExecutionPhase::Event);
4684 context.set_state("count", Dynamic::from(7_i64)).unwrap();
4685 let mut runtime = RuntimeEngine::new();
4686 let dark = crate::load_theme_source(
4687 runtime.engine(),
4688 "default_dark.rhai",
4689 include_str!("../../../registry/themes/default_dark.rhai"),
4690 )
4691 .unwrap();
4692 let mocha = crate::load_theme_source(
4693 runtime.engine(),
4694 "catppuccin_mocha.rhai",
4695 include_str!("../../../registry/themes/catppuccin_mocha.rhai"),
4696 )
4697 .unwrap();
4698 context.runtime().borrow_mut().theme = Some(
4699 ThemeManager::from_variants([dark, mocha], ThemeSelection::new("Default", "Dark"))
4700 .unwrap(),
4701 );
4702 let compiled = runtime
4703 .compile(
4704 r#"
4705 fn view() { text("theme") }
4706 fn switch_theme(ctx) { ctx.set_theme("Catppuccin", "Mocha"); }
4707 "#,
4708 )
4709 .unwrap();
4710 runtime.render(&compiled).unwrap();
4711 let generation = compiled.generation();
4712 let callback = runtime.callback(&compiled, "switch_theme").unwrap();
4713 let _ = runtime
4714 .invoke_callback(&compiled, &callback, (context.clone(),))
4715 .unwrap();
4716
4717 assert_eq!(context.get_state("count").unwrap(), UiValue::Integer(7));
4718 assert_eq!(compiled.generation(), generation);
4719 let state = context.runtime().borrow();
4720 let theme = state
4721 .theme
4722 .as_ref()
4723 .unwrap()
4724 .resolve(Some("main"), None, crate::SystemAppearance::Dark)
4725 .unwrap();
4726 assert_eq!(theme.variant().family, "Catppuccin");
4727 assert_eq!(theme.variant().name, "Mocha");
4728 drop(state);
4729 context.set_theme_system("Default").unwrap();
4730 let state = context.runtime().borrow();
4731 assert!(matches!(
4732 state.theme.as_ref().unwrap().app_preference(),
4733 ThemePreference::System { family } if family == "Default"
4734 ));
4735 }
4736
4737 #[test]
4738 fn script_reads_fixed_today_calendar_and_locale_formatters() {
4739 let mut engine = Engine::new();
4740 register_ui_context_api(&mut engine);
4741 let bundle = crate::load_locale_source(
4742 &engine,
4743 "en.rhai",
4744 include_str!("../../../registry/locales/en.rhai"),
4745 )
4746 .unwrap();
4747 let mut state = UiRuntimeState::new();
4748 state.locale = Some(LocaleManager::new([bundle], "en", "en").unwrap());
4749 state.calendar_clock =
4750 crate::CalendarClock::fixed(crate::GregorianDate::parse_iso("2026-08-29").unwrap());
4751 let context = UiContext::new(
4752 Rc::new(RefCell::new(state)),
4753 ComponentInstancePath::root("App", "root"),
4754 Some("main".to_owned()),
4755 ExecutionPhase::Render,
4756 BTreeMap::new(),
4757 );
4758 let mut scope = rhai::Scope::new();
4759 scope.push("ctx", context);
4760 let values = engine
4761 .eval_with_scope::<rhai::Array>(
4762 &mut scope,
4763 r#"[
4764 ctx.today(),
4765 ctx.format_date("2024-02-29", "long"),
4766 ctx.format_number(12345, #{ min_fraction_digits: 0, max_fraction_digits: 0 }),
4767 ctx.calendar().first_weekday,
4768 ]"#,
4769 )
4770 .unwrap();
4771 assert_eq!(values[0].clone_cast::<String>(), "2026-08-29");
4772 assert_eq!(
4773 values[1].clone_cast::<String>(),
4774 "Thursday, February 29, 2024"
4775 );
4776 assert_eq!(values[2].clone_cast::<String>(), "12,345");
4777 assert_eq!(values[3].clone_cast::<String>(), "sunday");
4778 }
4779
4780 #[test]
4781 fn returning_subscription_work_closes_external_emitters_with_reason() {
4782 let mut engine = RuntimeEngine::new();
4783 let compiled = engine
4784 .compile(
4785 r#"
4786 fn view() { text("subscription") }
4787 fn success(ctx, value) { value }
4788 fn failure(ctx, error) { error }
4789 "#,
4790 )
4791 .unwrap();
4792 let generation = compiled.generation();
4793 let success = engine.callback(&compiled, "success").unwrap();
4794 let failure = engine.callback(&compiled, "failure").unwrap();
4795 let mut state = UiRuntimeState::new();
4796 let registration = SubscriptionRegistration::new(
4797 "app.stream.watch",
4798 AsyncScope::App,
4799 generation,
4800 success,
4801 failure,
4802 ValueSchema::integer(),
4803 );
4804 let (_, emitter) = state.subscriptions.subscribe(registration);
4805 let external = emitter.clone();
4806 run_subscription_work(SubscriptionWork::new(|_| {}), emitter.clone(), &emitter);
4807
4808 assert!(matches!(
4809 external.emit(UiValue::Integer(1)),
4810 Err(AsyncRuntimeError::Closed {
4811 reason: SubscriptionCloseReason::WorkReturned
4812 })
4813 ));
4814 let _ = state.subscriptions.drain(generation);
4815 assert_eq!(
4816 state.subscriptions.take_closures()[0].reason,
4817 SubscriptionCloseReason::WorkReturned
4818 );
4819 let registration = SubscriptionRegistration::new(
4820 "app.stream.trace",
4821 AsyncScope::App,
4822 generation,
4823 engine.callback(&compiled, "success").unwrap(),
4824 engine.callback(&compiled, "failure").unwrap(),
4825 ValueSchema::integer(),
4826 );
4827 let (_, emitter) = state.subscriptions.subscribe(registration);
4828 emitter.close_with_reason(SubscriptionCloseReason::WorkReturned);
4829 let _ = state.subscriptions.drain(generation);
4830 state.trace_subscription_closures();
4831 let traces = state.traces.snapshot();
4832 assert!(traces.iter().any(|trace| {
4833 trace.kind == crate::RuntimeTraceKind::Subscription
4834 && trace.message == "close app.stream.trace: the subscription work returned"
4835 }));
4836 }
4837
4838 #[test]
4839 fn receiver_subscription_forwards_external_values_until_sender_drop() {
4840 let mut engine = RuntimeEngine::new();
4841 let compiled = engine
4842 .compile(
4843 r#"
4844 fn view() { text("subscription") }
4845 fn success(ctx, value) { value }
4846 fn failure(ctx, error) { error }
4847 "#,
4848 )
4849 .unwrap();
4850 let generation = compiled.generation();
4851 let registration = SubscriptionRegistration::new(
4852 "app.stream.receiver",
4853 AsyncScope::App,
4854 generation,
4855 engine.callback(&compiled, "success").unwrap(),
4856 engine.callback(&compiled, "failure").unwrap(),
4857 ValueSchema::integer(),
4858 );
4859 let mut subscriptions = SubscriptionRegistry::new();
4860 let (_, emitter) = subscriptions.subscribe(registration);
4861 let external = emitter.clone();
4862 let closer = emitter.clone();
4863 let (sender, receiver) = std::sync::mpsc::channel();
4864 let worker = std::thread::spawn(move || {
4865 run_subscription_work(SubscriptionWork::from_receiver(receiver), emitter, &closer);
4866 });
4867 sender.send(UiValue::Integer(42)).unwrap();
4868 drop(sender);
4869 worker.join().unwrap();
4870
4871 let deliveries = subscriptions.drain(generation);
4872 assert_eq!(deliveries[0].payload, UiValue::Integer(42));
4873 assert!(matches!(
4874 external.emit(UiValue::Integer(43)),
4875 Err(AsyncRuntimeError::Closed {
4876 reason: SubscriptionCloseReason::WorkReturned
4877 })
4878 ));
4879 }
4880
4881 #[test]
4882 fn receiver_subscription_waits_for_lossless_capacity_without_ending_the_stream() {
4883 let mut engine = RuntimeEngine::new();
4884 let compiled = engine
4885 .compile(
4886 "fn view() { text(\"stream\") } fn success(ctx, value) {} fn failure(ctx, value) {}",
4887 )
4888 .unwrap();
4889 engine.render(&compiled).unwrap();
4890 let generation = compiled.generation();
4891 let registration = SubscriptionRegistration::new(
4892 "stream",
4893 AsyncScope::App,
4894 generation,
4895 engine.callback(&compiled, "success").unwrap(),
4896 engine.callback(&compiled, "failure").unwrap(),
4897 ValueSchema::integer(),
4898 )
4899 .with_capacity(1)
4900 .unwrap();
4901 let mut subscriptions = SubscriptionRegistry::new();
4902 let (_, emitter) = subscriptions.subscribe(registration);
4903 let closer = emitter.clone();
4904 let (sender, receiver) = std::sync::mpsc::channel();
4905 let worker = std::thread::spawn(move || {
4906 run_subscription_work(SubscriptionWork::from_receiver(receiver), emitter, &closer);
4907 });
4908 sender.send(UiValue::Integer(1)).unwrap();
4909 sender.send(UiValue::Integer(2)).unwrap();
4910 let deadline = std::time::Instant::now() + Duration::from_secs(1);
4911 let first = loop {
4912 let values = subscriptions.drain(generation);
4913 if !values.is_empty() {
4914 break values;
4915 }
4916 assert!(std::time::Instant::now() < deadline);
4917 std::thread::yield_now();
4918 };
4919 assert_eq!(first[0].payload, UiValue::Integer(1));
4920 let second = loop {
4921 let values = subscriptions.drain(generation);
4922 if !values.is_empty() {
4923 break values;
4924 }
4925 assert!(std::time::Instant::now() < deadline);
4926 std::thread::yield_now();
4927 };
4928 assert_eq!(second[0].payload, UiValue::Integer(2));
4929 assert_eq!(subscriptions.active_count(), 1);
4930 drop(sender);
4931 worker.join().unwrap();
4932 }
4933
4934 #[test]
4935 fn runtime_transaction_rollback_restores_cancelled_subscription() {
4936 let mut engine = RuntimeEngine::new();
4937 let compiled = engine
4938 .compile(
4939 "fn view() { text(\"test\") } fn success(ctx, value) {} fn failure(ctx, value) {}",
4940 )
4941 .unwrap();
4942 engine.render(&compiled).unwrap();
4943 let success = engine.callback(&compiled, "success").unwrap();
4944 let error = engine.callback(&compiled, "failure").unwrap();
4945 let generation = compiled.generation();
4946 let mut state = UiRuntimeState::new();
4947 let (handle, emitter) = state.subscriptions.subscribe(SubscriptionRegistration::new(
4948 "rollback",
4949 AsyncScope::App,
4950 generation,
4951 success,
4952 error,
4953 ValueSchema::integer(),
4954 ));
4955 let checkpoint = state.begin_transaction().unwrap();
4956 assert!(state.subscriptions.cancel(handle));
4957 assert_eq!(emitter.close_reason(), None);
4958 state.rollback_transaction(checkpoint).unwrap();
4959 assert_eq!(state.subscriptions.active_count(), 1);
4960 emitter.emit(UiValue::Integer(7)).unwrap();
4961 assert_eq!(state.subscriptions.drain(generation).len(), 1);
4962 }
4963
4964 #[test]
4965 fn transaction_tokens_cannot_cross_runtime_or_commit_unbalanced() {
4966 let mut first = UiRuntimeState::new();
4967 let mut second = UiRuntimeState::new();
4968 let checkpoint = first.begin_transaction().unwrap();
4969 assert!(matches!(
4970 second.rollback_transaction(checkpoint.clone()),
4971 Err(UiTransactionError::ForeignSnapshot)
4972 ));
4973 assert!(matches!(
4974 second.commit_transaction(),
4975 Err(UiTransactionError::Unbalanced { .. })
4976 ));
4977 first.rollback_transaction(checkpoint).unwrap();
4978 }
4979
4980 #[test]
4981 fn presentation_geometry_and_capture_are_isolated_per_view() {
4982 let mut first = crate::RetainedUiTree::new();
4983 first.reconcile(crate::UiNode::text("first")).unwrap();
4984 let mut second = crate::RetainedUiTree::new();
4985 second.reconcile(crate::UiNode::text("second")).unwrap();
4986 let first_node = first.root_id().unwrap();
4987 let second_node = second.root_id().unwrap();
4988 assert_eq!(
4989 first_node, second_node,
4990 "tree-local IDs intentionally overlap"
4991 );
4992
4993 let mut state = UiRuntimeState::new();
4994 state.ensure_presentation("first");
4995 state.ensure_presentation("second");
4996 let first_geometry = state.geometry_for(Some("first"));
4997 let second_geometry = state.geometry_for(Some("second"));
4998 let bounds = |width| crate::ElementGeometry {
4999 layout: crate::GeometryBounds::new(0.0, 0.0, width, 20.0).unwrap(),
5000 visual: crate::GeometryBounds::new(0.0, 0.0, width, 20.0).unwrap(),
5001 clip: None,
5002 };
5003 first_geometry.update(first_node, bounds(100.0));
5004 second_geometry.update(second_node, bounds(300.0));
5005 second_geometry.begin_frame();
5006 assert!(
5007 (first_geometry.get(first_node).unwrap().visual.width - 100.0).abs() < f64::EPSILON
5008 );
5009 assert!(first_geometry.is_presented(first_node));
5010
5011 let first_capture = state.pointer_capture_for(Some("first"));
5012 let second_capture = state.pointer_capture_for(Some("second"));
5013 first_capture.capture(0, first_node);
5014 second_capture.capture(0, second_node);
5015 assert_eq!(first_capture.captured(0), Some(first_node));
5016 assert_eq!(second_capture.captured(0), Some(second_node));
5017 }
5018
5019 #[test]
5020 fn window_release_uses_segment_boundaries_and_clears_suspend_tombstones() {
5021 let now = std::time::Instant::now();
5022 let transition = || {
5023 let mut spec =
5024 crate::MotionTransition::new(crate::MotionProperty::Opacity, 0.0, 1.0, 1_000);
5025 spec.easing = crate::MotionEasing::Linear;
5026 crate::MotionSource::Transition(spec)
5027 };
5028 let mut state = UiRuntimeState::new();
5029 let ghost_path = "window:w2/view:v2/root/ghost:1";
5030 state
5031 .motions
5032 .start_exit(
5033 ComponentInstancePath::root("UiNode", ghost_path),
5034 transition(),
5035 now,
5036 )
5037 .unwrap();
5038 state.motion_ghosts.push(crate::motion::MotionGhost {
5039 id: "1".to_owned(),
5040 node: crate::UiNode::text("ghost"),
5041 bounds: crate::GeometryBounds::new(0.0, 0.0, 10.0, 10.0).unwrap(),
5042 path: ghost_path.to_owned(),
5043 domain: "window:w2/view:v2/root".to_owned(),
5044 });
5045 state
5046 .release_window("w", &ComponentInstancePath::root("View", "v"))
5047 .unwrap();
5048 assert_eq!(state.motion_ghosts.len(), 1);
5049 assert_eq!(state.motions.resource_usage().active, 1);
5050
5051 let mut reopened = UiRuntimeState::new();
5052 let scope = "window:w/view:v/root";
5053 reopened
5054 .motions
5055 .start(
5056 ComponentInstancePath::root("UiNode", format!("{scope}/node:1")),
5057 transition(),
5058 now,
5059 )
5060 .unwrap();
5061 reopened
5062 .motions
5063 .suspend_node_scope(scope, now + Duration::from_millis(200));
5064 reopened
5065 .release_window("w", &ComponentInstancePath::root("View", "v"))
5066 .unwrap();
5067 let key = reopened
5068 .motions
5069 .start(
5070 ComponentInstancePath::root("UiNode", format!("{scope}/node:2")),
5071 transition(),
5072 now + Duration::from_millis(200),
5073 )
5074 .unwrap();
5075 let frame = reopened.motions.tick(now + Duration::from_millis(400));
5076 assert!(frame.needs_frame);
5077 assert!((frame.values[&key] - 0.2).abs() < 0.01);
5078 }
5079
5080 #[test]
5081 fn timeline_handles_cannot_cross_view_contexts() {
5082 let runtime = Rc::new(RefCell::new(UiRuntimeState::new()));
5083 let now = std::time::Instant::now();
5084 let mut transition =
5085 crate::MotionTransition::new(crate::MotionProperty::Opacity, 0.0, 1.0, 1_000);
5086 transition.easing = crate::MotionEasing::Linear;
5087 let handle = runtime
5088 .borrow_mut()
5089 .motions
5090 .start_timeline(
5091 ComponentInstancePath::root("UiNode", "window:a/view:a/root"),
5092 crate::MotionTimeline::new(
5093 "foreign",
5094 crate::MotionTimelineStep::Track(crate::MotionTrack {
5095 target: ".".to_owned(),
5096 source: crate::MotionSource::Transition(transition),
5097 }),
5098 ),
5099 now,
5100 )
5101 .unwrap();
5102 let foreign = UiContext::new(
5103 Rc::clone(&runtime),
5104 ComponentInstancePath::root("View", "b"),
5105 Some("b".to_owned()),
5106 ExecutionPhase::Event,
5107 BTreeMap::new(),
5108 )
5109 .with_view_id("b");
5110
5111 assert!(matches!(
5112 foreign.cancel_motion(&handle),
5113 Err(UiContextError::Motion(
5114 crate::MotionError::ForeignTimelineHandle(_)
5115 ))
5116 ));
5117 assert_eq!(
5118 runtime.borrow().motions.timeline_state(&handle),
5119 Some(crate::MotionPlaybackState::Playing)
5120 );
5121 }
5122
5123 #[test]
5124 fn focus_by_ref_key_queues_a_window_scoped_retained_command() {
5125 let context = mounted_context(ExecutionPhase::Event);
5126 let mut tree = crate::RetainedUiTree::new();
5127 tree.reconcile(crate::UiNode::text("field")).unwrap();
5128 let reference = crate::ElementRef::new(
5129 crate::ElementRefId::new(context.component_path().clone(), "field").unwrap(),
5130 );
5131 context.runtime().borrow_mut().element_refs.reconcile(
5132 context.component_path(),
5133 BTreeMap::from([(reference.id().clone(), tree.root_id().unwrap())]),
5134 );
5135 let node = tree.root_id().unwrap();
5136 context
5137 .runtime()
5138 .borrow()
5139 .geometry_for(Some("main"))
5140 .update(
5141 node,
5142 crate::ElementGeometry {
5143 layout: crate::GeometryBounds::new(1.0, 2.0, 120.0, 24.0).unwrap(),
5144 visual: crate::GeometryBounds::new(3.0, 4.0, 120.0, 24.0).unwrap(),
5145 clip: None,
5146 },
5147 );
5148 assert!(matches!(
5149 context.element_bounds_by_key("field").unwrap(),
5150 UiValue::Map(bounds)
5151 if matches!(bounds.get("visual"), Some(UiValue::Map(visual))
5152 if visual.get("width") == Some(&UiValue::Float(120.0)))
5153 ));
5154 context.focus_element_by_key("field").unwrap();
5155 context
5156 .scroll_element_to_by_key("field", 12.0, 24.0)
5157 .unwrap();
5158 context.scroll_element_into_view_by_key("field").unwrap();
5159 let commands = context
5160 .runtime()
5161 .borrow_mut()
5162 .take_window_element_commands("main");
5163 assert!(matches!(
5164 commands.as_slice(),
5165 [crate::element_ref::ElementCommand::Focus { node, .. },
5166 crate::element_ref::ElementCommand::ScrollTo { node: scroll_node, x, y, .. },
5167 crate::element_ref::ElementCommand::ScrollIntoView { node: reveal_node, .. }]
5168 if *node == tree.root_id().unwrap()
5169 && scroll_node == node
5170 && reveal_node == node
5171 && (*x - 12.0).abs() < f64::EPSILON
5172 && (*y - 24.0).abs() < f64::EPSILON
5173 ));
5174 }
5175
5176 #[test]
5177 fn host_motion_preference_changes_without_replacing_runtime_state() {
5178 let mut runtime = UiRuntimeState::new();
5179 runtime.motions = crate::MotionRuntime::new(crate::MotionPreference::Normal);
5180 runtime.responsive.update_window("main", 480.0).unwrap();
5181
5182 assert!(!runtime.set_motion_preference_from_host(crate::MotionPreference::Normal));
5183 assert!(runtime.set_motion_preference_from_host(crate::MotionPreference::Reduced));
5184 assert_eq!(
5185 runtime.motions.preference(),
5186 crate::MotionPreference::Reduced
5187 );
5188 assert_eq!(
5189 runtime.responsive.class("main"),
5190 crate::ViewportClass::Compact
5191 );
5192 assert!(runtime.set_motion_preference_from_host(crate::MotionPreference::None));
5193 assert_eq!(runtime.motions.preference(), crate::MotionPreference::None);
5194 }
5195}