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 key_bindings: Vec<crate::KeyBindingSpec>,
101 pub audit_rules: crate::AuditRules,
103 pub capabilities: CapabilityRegistry,
104 pub tasks: TaskRegistry,
105 pub subscriptions: SubscriptionRegistry,
106 pub timers: crate::TimerRegistry,
107 pub locale: Option<LocaleManager>,
108 pub calendar_clock: CalendarClock,
109 pub clock: crate::RuntimeClock,
110 pub theme: Option<ThemeManager>,
111 component_styles: crate::ComponentStyleSheet,
112 pub assets: AssetRegistry,
113 pub motions: MotionRuntime,
114 pub effects: crate::EffectRegistry,
115 pub signals: crate::SignalRegistry,
116 pub element_refs: crate::ElementRefRegistry,
117 pub(crate) geometry: crate::GeometryRegistry,
118 pub(crate) pointer_capture: crate::PointerCaptureRegistry,
119 presentations: BTreeMap<String, ViewPresentationState>,
120 pub budgets: crate::RuntimeBudgets,
121 pub virtual_requests: crate::VirtualRequestRegistry,
122 pub windows: WindowCommandRegistry,
123 pub responsive: ResponsiveRuntime,
124 pub(crate) environment_dependencies:
125 crate::environment_dependency::EnvironmentDependencyRegistry,
126 pub motion_values: BTreeMap<MotionKey, f64>,
127 pub(crate) motion_ghosts: Vec<crate::motion::MotionGhost>,
128 pub(crate) window_appearances: BTreeMap<String, crate::SystemAppearance>,
129 pub traces: crate::TraceBuffer,
130 component_event_handlers: BTreeMap<(ComponentInstancePath, String), ScriptCallback>,
131 component_incarnations: BTreeMap<ComponentInstancePath, ComponentIncarnation>,
132 next_component_incarnation: u64,
133 dirty: BTreeSet<ComponentInstancePath>,
134 pending_events: Vec<PendingEvent>,
135 pending_actions: Vec<ActionInvocation>,
136 pending_async: Vec<crate::AsyncDelivery>,
137 pending_element_commands: Vec<crate::element_ref::ElementCommand>,
138 repaint_windows: BTreeSet<String>,
139 component_style_generation: u64,
140 origin: crate::InvocationOrigin,
142}
143
144impl UiRuntimeState {
145 #[must_use]
147 pub fn invocation_origin(&self) -> &crate::InvocationOrigin {
148 &self.origin
149 }
150
151 pub(crate) fn replace_origin(
154 &mut self,
155 origin: crate::InvocationOrigin,
156 ) -> crate::InvocationOrigin {
157 self.tasks.set_origin_root(&origin);
158 self.timers.set_origin_root(&origin);
159 std::mem::replace(&mut self.origin, origin)
160 }
161
162 pub(crate) fn update_window_appearance(
163 &mut self,
164 window: &str,
165 appearance: crate::SystemAppearance,
166 ) -> bool {
167 let previous = self
168 .window_appearances
169 .insert(window.to_owned(), appearance)
170 .unwrap_or(crate::SystemAppearance::Dark);
171 let changed = previous != appearance;
172 if changed {
173 let invalidated = self
174 .environment_dependencies
175 .invalidate_theme_window(window);
176 self.mark_dirty(invalidated);
177 }
178 changed
179 }
180 const SUSPENDED_DELIVERY_CAPACITY: usize = 256;
181 #[must_use]
182 pub fn new() -> Self {
183 Self::default()
184 }
185
186 pub(crate) fn ensure_component_incarnation(
187 &mut self,
188 component: &ComponentInstancePath,
189 ) -> ComponentIncarnation {
190 if let Some(incarnation) = self.component_incarnations.get(component) {
191 return *incarnation;
192 }
193 let next = self.next_component_incarnation.max(1);
194 let incarnation = ComponentIncarnation(next);
195 self.next_component_incarnation = next.saturating_add(1);
196 self.component_incarnations
197 .insert(component.clone(), incarnation);
198 incarnation
199 }
200
201 pub(crate) fn component_incarnation(
202 &self,
203 component: &ComponentInstancePath,
204 ) -> Option<ComponentIncarnation> {
205 self.component_incarnations.get(component).copied()
206 }
207
208 pub(crate) fn component_incarnations_snapshot(
209 &self,
210 ) -> BTreeMap<ComponentInstancePath, ComponentIncarnation> {
211 self.component_incarnations.clone()
212 }
213
214 pub(crate) fn ensure_presentation(&mut self, view: &str) {
215 self.presentations.entry(view.to_owned()).or_default();
216 }
217
218 pub fn presentation_geometry(&mut self, view: &str) -> crate::GeometryRegistry {
220 self.ensure_presentation(view);
221 self.geometry_for(Some(view))
222 }
223
224 pub fn presentation_pointer_capture(&mut self, view: &str) -> crate::PointerCaptureRegistry {
226 self.ensure_presentation(view);
227 self.pointer_capture_for(Some(view))
228 }
229
230 pub(crate) fn geometry_for(&self, view: Option<&str>) -> crate::GeometryRegistry {
231 view.and_then(|view| self.presentations.get(view))
232 .map_or_else(|| self.geometry.clone(), |state| state.geometry.clone())
233 }
234
235 pub(crate) fn pointer_capture_for(&self, view: Option<&str>) -> crate::PointerCaptureRegistry {
236 view.and_then(|view| self.presentations.get(view))
237 .map_or_else(
238 || self.pointer_capture.clone(),
239 |state| state.pointer_capture.clone(),
240 )
241 }
242
243 pub(crate) fn remove_presentation(&mut self, view: &str) {
244 self.presentations.remove(view);
245 }
246
247 pub fn set_component_state_from_host(
254 &mut self,
255 component: &ComponentInstancePath,
256 field: &str,
257 value: UiValue,
258 ) -> Result<bool, crate::StateError> {
259 let changed = self.component_state.set(component, field, value)?;
260 if changed {
261 self.dirty.insert(component.clone());
262 }
263 Ok(changed)
264 }
265
266 pub fn replace_native_collection_from_host(
272 &mut self,
273 name: &str,
274 collection: crate::NativeCollection,
275 ) -> Result<bool, crate::NativeCollectionError> {
276 let invalidated = self.native_collections.replace(name, collection)?;
277 let changed = !invalidated.is_empty();
278 self.dirty.extend(invalidated);
279 Ok(changed)
280 }
281
282 pub fn register_native_collection_from_host(
289 &mut self,
290 _root: &ComponentInstancePath,
291 name: &str,
292 collection: crate::NativeCollection,
293 ) -> Result<(), crate::NativeCollectionError> {
294 let invalidated = self
295 .native_collections
296 .register_with_invalidated(name.to_owned(), collection)?;
297 self.dirty.extend(invalidated);
298 Ok(())
299 }
300
301 pub fn replace_native_text_document_from_host(
307 &mut self,
308 name: &str,
309 document: crate::NativeTextDocument,
310 ) -> Result<bool, crate::DocumentError> {
311 let invalidated = self.native_documents.replace(name, document)?;
312 let changed = !invalidated.is_empty();
313 self.dirty.extend(invalidated);
314 Ok(changed)
315 }
316
317 #[cfg(feature = "charts")]
323 pub fn register_native_chart_data(
324 &mut self,
325 name: impl Into<String>,
326 data: crate::NativeChartData,
327 ) -> Result<(), UiContextError> {
328 let name = name.into();
329 if name.is_empty()
330 || name.len() > 128
331 || !name
332 .chars()
333 .next()
334 .is_some_and(|character| character.is_ascii_alphabetic())
335 || !name
336 .chars()
337 .all(|character| character.is_ascii_alphanumeric() || character == '_')
338 {
339 return Err(UiContextError::InvalidChartDataName(name));
340 }
341 self.native_chart_data.insert(name, data);
342 Ok(())
343 }
344
345 pub fn replace_theme_variant_from_host(
352 &mut self,
353 variant: crate::ThemeVariant,
354 ) -> Result<(), crate::ThemeError> {
355 self.theme
356 .as_mut()
357 .ok_or(crate::ThemeError::NoVariants)?
358 .replace_variant(variant)?;
359 self.dirty.extend(self.component_state.paths());
360 Ok(())
361 }
362
363 pub fn select_theme_from_host(
369 &mut self,
370 family: &str,
371 variant: &str,
372 ) -> Result<bool, crate::ThemeError> {
373 let theme = self.theme.as_mut().ok_or(crate::ThemeError::NoVariants)?;
374 let previous = theme.generation();
375 theme.set_app(ThemePreference::Fixed {
376 selection: ThemeSelection::new(family, variant),
377 })?;
378 let changed = theme.generation() != previous;
379 if changed {
380 let invalidated = self.environment_dependencies.invalidate_theme_app();
381 self.dirty.extend(invalidated);
382 self.mark_all_windows_dirty();
383 }
384 Ok(changed)
385 }
386
387 pub fn select_locale_from_host(&mut self, locale: &str) -> Result<bool, crate::LocaleError> {
393 let locales = self
394 .locale
395 .as_mut()
396 .ok_or_else(|| crate::LocaleError::UnknownLocale(locale.to_owned()))?;
397 let previous = locales.generation();
398 locales.set_app(locale)?;
399 let changed = locales.generation() != previous;
400 if changed {
401 let invalidated = self.environment_dependencies.invalidate_locale_app();
402 self.dirty.extend(invalidated);
403 self.mark_all_windows_repaint();
404 }
405 Ok(changed)
406 }
407
408 pub fn set_motion_preference_from_host(&mut self, preference: crate::MotionPreference) -> bool {
414 let previous = self.motions.preference();
415 self.motions.set_preference(preference);
416 let changed = self.motions.preference() != previous;
417 if changed {
418 let now = self.clock.now();
419 self.motion_values = self.motions.snapshot(now);
420 self.mark_all_windows_dirty();
421 }
422 changed
423 }
424
425 pub fn replace_component_styles_from_host(
430 &mut self,
431 styles: crate::ComponentStyleSheet,
432 ) -> bool {
433 if self.component_styles == styles {
434 return false;
435 }
436 self.component_styles = styles;
437 self.component_style_generation = self.component_style_generation.saturating_add(1);
438 self.dirty.extend(self.component_state.paths());
439 true
440 }
441
442 pub(crate) const fn component_style_generation(&self) -> u64 {
443 self.component_style_generation
444 }
445
446 pub(crate) const fn component_styles(&self) -> &crate::ComponentStyleSheet {
447 &self.component_styles
448 }
449
450 pub(crate) fn trace_subscription_closures(&mut self) {
451 for closure in self.subscriptions.take_closures() {
452 let scope = match closure.scope {
453 AsyncScope::App => "/App".to_owned(),
454 AsyncScope::Window(window) => format!("window:{window}"),
455 AsyncScope::Component(component) => component.to_string(),
456 AsyncScope::Effect {
457 component,
458 key,
459 activation,
460 } => format!("{component}/effect[{key}]#{activation}"),
461 };
462 self.traces.push(
463 crate::RuntimeTraceKind::Subscription,
464 scope,
465 format!("close {}: {}", closure.label, closure.reason),
466 None,
467 false,
468 );
469 }
470 }
471
472 pub fn release_window(
478 &mut self,
479 window: &str,
480 root: &ComponentInstancePath,
481 ) -> Result<(), AssetError> {
482 self.tasks
483 .cancel_scope(&AsyncScope::Window(window.to_owned()));
484 self.tasks.cancel_component_scope(root);
485 self.timers.cancel_component_scope(root);
486 self.subscriptions
487 .cancel_scope(&AsyncScope::Window(window.to_owned()));
488 self.subscriptions.cancel_component_scope(root);
489 self.assets.cancel_window_scope(window, root)?;
490 self.stores.remove_window(window);
491 self.native_collections.remove_reader_scope(root);
492 self.native_documents.remove_reader_scope(root);
493 self.component_state.remove_scope(root);
494 self.component_event_handlers
495 .retain(|(path, _), _| !path.is_within(root));
496 self.component_incarnations
497 .retain(|path, _| !path.is_within(root));
498 self.actions.remove_component_scope(root);
499 self.dirty.retain(|path| !path.is_within(root));
500 self.pending_events
501 .retain(|event| !event.target.is_within(root));
502 self.pending_actions.retain(|action| {
503 !action
504 .callback
505 .component()
506 .is_some_and(|path| path.is_within(root))
507 });
508 self.pending_async.retain(|delivery| match &delivery.scope {
509 AsyncScope::App => true,
510 AsyncScope::Window(id) => id != window,
511 AsyncScope::Component(_) | AsyncScope::Effect { .. } => {
512 !delivery.scope.is_within_component(root)
513 }
514 });
515 self.pending_element_commands
516 .retain(|command| command.window() != window);
517 self.environment_dependencies.remove_window(window);
518 self.environment_dependencies.remove_scope(root);
519 self.repaint_windows.remove(window);
520 self.window_appearances.remove(window);
521 if let Some(theme) = self.theme.as_mut() {
522 theme.remove_window(window);
523 theme.remove_scope(root);
524 }
525 if let Some(locale) = self.locale.as_mut() {
526 locale.remove_window(window);
527 locale.remove_scope(root);
528 }
529 let window_scope = format!("window:{window}");
530 self.motions.release_node_scope(&window_scope);
531 let removed_ghosts = self
532 .motion_ghosts
533 .iter()
534 .filter(|ghost| domain_is_within(&ghost.domain, &window_scope))
535 .map(|ghost| ghost.path.clone())
536 .collect::<Vec<_>>();
537 self.motion_ghosts
538 .retain(|ghost| !domain_is_within(&ghost.domain, &window_scope));
539 for path in removed_ghosts {
540 self.motions.cancel_node_scope(&path);
541 }
542 self.motions.discard_timeline_events_in_scope(&window_scope);
543 self.motion_values = self.motions.snapshot(self.clock.now());
544 self.effects.remove_scope(root);
545 self.signals.remove_scope(root);
546 self.element_refs.remove_scope(root);
547 self.geometry.remove_scope(root);
548 for presentation in self.presentations.values() {
549 presentation.geometry.remove_scope(root);
550 }
551 self.windows.remove(window);
552 self.responsive.remove_window(window);
553 Ok(())
554 }
555
556 pub(crate) fn queue_async(
557 &mut self,
558 deliveries: impl IntoIterator<Item = crate::AsyncDelivery>,
559 ) {
560 self.pending_async.extend(deliveries);
561 }
562
563 pub(crate) fn queue_suspended_async(
564 &mut self,
565 deliveries: impl IntoIterator<Item = crate::AsyncDelivery>,
566 ) -> Result<(), AsyncRuntimeError> {
567 for delivery in deliveries {
568 if self.pending_async.len() >= Self::SUSPENDED_DELIVERY_CAPACITY {
569 return Err(AsyncRuntimeError::SuspendedBackpressure {
570 capacity: Self::SUSPENDED_DELIVERY_CAPACITY,
571 });
572 }
573 self.pending_async.push(delivery);
574 }
575 Ok(())
576 }
577
578 pub(crate) fn suspended_delivery_capacity_remaining(&self) -> usize {
579 Self::SUSPENDED_DELIVERY_CAPACITY.saturating_sub(self.pending_async.len())
580 }
581
582 pub(crate) fn cancel_async_scope(&mut self, scope: &AsyncScope) -> Result<(), AssetError> {
583 self.assets.cancel_scope(scope)?;
584 self.tasks.cancel_scope(scope);
585 self.subscriptions.cancel_scope(scope);
586 self.pending_async
587 .retain(|delivery| &delivery.scope != scope);
588 self.trace_subscription_closures();
589 Ok(())
590 }
591
592 pub(crate) fn take_window_async(
593 &mut self,
594 window: &str,
595 root: &ComponentInstancePath,
596 ) -> Vec<crate::AsyncDelivery> {
597 let app_owner = self.windows.open_ids().into_iter().next();
598 let pending = std::mem::take(&mut self.pending_async);
599 let (accepted, retained) =
600 pending
601 .into_iter()
602 .partition(|delivery| match &delivery.scope {
603 AsyncScope::App => app_owner.as_deref() == Some(window),
604 AsyncScope::Window(id) => id == window,
605 AsyncScope::Component(_) | AsyncScope::Effect { .. } => {
606 delivery.scope.is_within_component(root)
607 }
608 });
609 self.pending_async = retained;
610 accepted
611 }
612
613 pub(crate) fn discard_component_async_before_generation(
614 &mut self,
615 root: &ComponentInstancePath,
616 generation: crate::ScriptGeneration,
617 ) {
618 self.pending_async.retain(|delivery| {
619 !delivery.scope.is_within_component(root)
620 || delivery.callback.generation() == generation
621 });
622 }
623
624 pub(crate) fn requeue_element_commands(
626 &mut self,
627 commands: Vec<crate::element_ref::ElementCommand>,
628 ) {
629 self.pending_element_commands.extend(commands);
630 }
631
632 pub(crate) fn take_window_element_commands(
633 &mut self,
634 window: &str,
635 ) -> Vec<crate::element_ref::ElementCommand> {
636 let commands = std::mem::take(&mut self.pending_element_commands);
637 let (selected, retained) = commands
638 .into_iter()
639 .partition(|command| command.window() == window);
640 self.pending_element_commands = retained;
641 selected
642 }
643
644 pub(crate) fn has_window_dirty(&self, root: &ComponentInstancePath) -> bool {
645 self.dirty.iter().any(|path| path.is_within(root))
646 }
647
648 pub(crate) fn flush_geometry_dependencies(&mut self) {
649 self.dirty.extend(self.element_refs.take_dirty());
650 self.dirty.extend(self.geometry.take_dirty());
651 for presentation in self.presentations.values() {
652 self.dirty.extend(presentation.geometry.take_dirty());
653 }
654 }
655
656 pub(crate) fn take_window_dirty_components(
657 &mut self,
658 root: &ComponentInstancePath,
659 ) -> BTreeSet<ComponentInstancePath> {
660 let all = std::mem::take(&mut self.dirty);
661 let (selected, retained) = all.into_iter().partition(|path| path.is_within(root));
662 self.dirty = retained;
663 selected
664 }
665
666 pub(crate) fn drain_pending_actions(&mut self) -> Vec<ActionInvocation> {
667 std::mem::take(&mut self.pending_actions)
668 }
669
670 pub(crate) fn drain_pending_events(&mut self) -> Vec<PendingEvent> {
671 std::mem::take(&mut self.pending_events)
672 }
673
674 pub(crate) fn has_pending_dispatch(&self) -> bool {
675 !self.pending_actions.is_empty() || !self.pending_events.is_empty()
676 }
677
678 pub(crate) fn has_virtual_requests(&self) -> bool {
679 !self.virtual_requests.is_empty()
680 }
681
682 pub(crate) fn component_event_handler(
683 &self,
684 component: &ComponentInstancePath,
685 event: &str,
686 ) -> Option<ScriptCallback> {
687 self.component_event_handlers
688 .get(&(component.clone(), event.to_owned()))
689 .cloned()
690 }
691
692 pub(crate) fn reset_component_readers(&mut self, component: &ComponentInstancePath) {
693 self.stores.reset_reader(component);
694 self.native_collections.reset_reader(component);
695 self.native_documents.reset_reader(component);
696 self.environment_dependencies.reset_reader(component);
697 self.reset_geometry_read_contribution(&crate::read_dependency::ReadDependency::component(
698 component,
699 ));
700 }
701
702 pub(crate) fn retain_virtual_read_contributions(
703 &mut self,
704 scope: &ComponentInstancePath,
705 active: &BTreeSet<crate::read_dependency::ReadContribution>,
706 ) {
707 self.native_collections.retain_contributions(scope, active);
708 self.native_documents.retain_contributions(scope, active);
709 self.stores.retain_contributions(scope, active);
710 self.environment_dependencies
711 .retain_contributions(scope, active);
712 self.element_refs.retain_contributions(scope, active);
713 self.geometry.retain_contributions(scope, active);
714 for presentation in self.presentations.values() {
715 presentation.geometry.retain_contributions(scope, active);
716 }
717 }
718
719 fn reset_geometry_read_contribution(
720 &mut self,
721 reader: &crate::read_dependency::ReadDependency,
722 ) {
723 self.element_refs.reset_contribution(reader);
724 self.geometry.reset_contribution(reader);
725 for presentation in self.presentations.values() {
726 presentation.geometry.reset_contribution(reader);
727 }
728 }
729
730 pub(crate) fn component_event_handlers_in_scope(
731 &self,
732 root: &ComponentInstancePath,
733 ) -> BTreeMap<(ComponentInstancePath, String), ScriptCallback> {
734 self.component_event_handlers
735 .iter()
736 .filter(|((path, _), _)| path.is_within(root))
737 .map(|(id, callback)| (id.clone(), callback.clone()))
738 .collect()
739 }
740
741 pub(crate) fn replace_component_event_handlers(
742 &mut self,
743 root: &ComponentInstancePath,
744 handlers: BTreeMap<(ComponentInstancePath, String), ScriptCallback>,
745 ) {
746 self.component_event_handlers
747 .retain(|(path, _), _| !path.is_within(root));
748 self.component_event_handlers.extend(handlers);
749 }
750
751 pub(crate) fn mark_all_windows_dirty(&mut self) {
752 self.dirty.extend(
753 self.windows
754 .open_ids()
755 .into_iter()
756 .map(|window| ComponentInstancePath::root("App", window)),
757 );
758 }
759
760 pub(crate) fn mark_dirty(
761 &mut self,
762 components: impl IntoIterator<Item = ComponentInstancePath>,
763 ) {
764 self.dirty.extend(components);
765 }
766
767 pub(crate) fn mark_all_windows_repaint(&mut self) {
768 self.repaint_windows.extend(self.windows.open_ids());
769 }
770
771 pub(crate) fn mark_window_repaint(&mut self, window: impl Into<String>) {
772 self.repaint_windows.insert(window.into());
773 }
774
775 pub(crate) fn take_window_repaint(&mut self, window: &str) -> bool {
776 self.repaint_windows.remove(window)
777 }
778
779 pub(crate) fn reconcile_component_lifetimes(
780 &mut self,
781 root: &ComponentInstancePath,
782 active: &BTreeSet<ComponentInstancePath>,
783 previous: &BTreeSet<ComponentInstancePath>,
784 ) -> Result<(), AssetError> {
785 for removed in previous
786 .iter()
787 .filter(|path| path.is_within(root) && *path != root && !active.contains(*path))
788 {
789 self.tasks.cancel_component_scope(removed);
790 self.subscriptions.cancel_component_scope(removed);
791 self.timers.cancel_component_scope(removed);
792 self.assets.cancel_component_scope(removed)?;
793 self.actions.remove_component_scope(removed);
794 }
795 self.pending_async.retain(|delivery| {
796 delivery
797 .scope
798 .component()
799 .is_none_or(|path| !path.is_within(root) || active.contains(path))
800 });
801 self.pending_actions.retain(|action| {
802 !action
803 .callback
804 .component()
805 .is_some_and(|path| path.is_within(root) && !active.contains(path))
806 });
807 self.pending_events.retain(|event| {
808 !event.target.is_within(root) || active.contains(&event.target) || event.target == *root
809 });
810 self.stores.retain_reader_scope(root, active);
811 self.component_event_handlers
812 .retain(|(path, _), _| !path.is_within(root) || path == root || active.contains(path));
813 self.native_collections.retain_reader_scope(root, active);
814 self.native_documents.retain_reader_scope(root, active);
815 self.element_refs.retain_scope(root, active);
816 self.geometry.retain_reader_scope(root, active);
817 for presentation in self.presentations.values() {
818 presentation.geometry.retain_reader_scope(root, active);
819 }
820 self.environment_dependencies.retain_scope(root, active);
821 self.dirty
822 .retain(|path| !path.is_within(root) || path == root || active.contains(path));
823 self.component_incarnations
824 .retain(|path, _| !path.is_within(root) || path == root || active.contains(path));
825 Ok(())
826 }
827
828 #[must_use]
829 pub fn drain_batch(&mut self) -> UiMutationBatch {
830 UiMutationBatch {
831 dirty: std::mem::take(&mut self.dirty),
832 events: std::mem::take(&mut self.pending_events),
833 actions: std::mem::take(&mut self.pending_actions),
834 }
835 }
836
837 #[must_use]
838 pub fn dirty_components(&self) -> &BTreeSet<ComponentInstancePath> {
839 &self.dirty
840 }
841
842 pub(crate) fn snapshot(&self) -> Result<UiStateSnapshot, AssetError> {
843 Ok(UiStateSnapshot {
844 runtime_id: self.runtime_id,
845 transaction_depth: self.transaction_depth,
846 component_state: self.component_state.clone(),
847 stores: self.stores.clone(),
848 native_collections: self.native_collections.clone(),
849 native_documents: self.native_documents.clone(),
850 actions: self.actions.clone(),
851 component_event_handlers: self.component_event_handlers.clone(),
852 component_incarnations: self.component_incarnations.clone(),
853 next_component_incarnation: self.next_component_incarnation,
854 dirty: self.dirty.clone(),
855 pending_events: self.pending_events.clone(),
856 pending_actions: self.pending_actions.clone(),
857 pending_async: self.pending_async.clone(),
858 pending_element_commands: self.pending_element_commands.clone(),
859 locale: self.locale.clone(),
860 theme: self.theme.clone(),
861 component_styles: self.component_styles.clone(),
862 component_style_generation: self.component_style_generation,
863 motions: self.motions.transaction_snapshot(),
864 effects: self.effects.clone(),
865 signals: self.signals.clone(),
866 element_refs: self.element_refs.clone(),
867 geometry: self.geometry.snapshot(),
868 pointer_capture: self.pointer_capture.snapshot(),
869 presentations: self
870 .presentations
871 .iter()
872 .map(|(view, state)| (view.clone(), state.snapshot()))
873 .collect(),
874 budgets: self.budgets.clone(),
875 virtual_requests: self.virtual_requests.snapshot(),
876 motion_values: self.motion_values.clone(),
877 motion_ghosts: self.motion_ghosts.clone(),
878 windows: self.windows.clone(),
879 responsive: self.responsive.clone(),
880 environment_dependencies: self.environment_dependencies.clone(),
881 repaint_windows: self.repaint_windows.clone(),
882 tasks: self.tasks.snapshot(),
883 subscriptions: self.subscriptions.snapshot(),
884 timers: self.timers.clone(),
885 image_decodes: self.assets.decode_snapshot()?,
886 })
887 }
888
889 pub(crate) fn restore(&mut self, snapshot: UiStateSnapshot) -> Result<(), UiTransactionError> {
890 if self.runtime_id != snapshot.runtime_id {
891 return Err(UiTransactionError::ForeignSnapshot);
892 }
893 let expected_depth = snapshot.transaction_depth.saturating_add(1);
894 if self.transaction_depth != expected_depth {
895 return Err(UiTransactionError::Unbalanced {
896 expected: expected_depth,
897 actual: self.transaction_depth,
898 });
899 }
900 self.tasks.restore(snapshot.tasks);
901 self.subscriptions.restore(snapshot.subscriptions);
902 self.timers = snapshot.timers;
903 let decode_result = self.assets.restore_decode_snapshot(snapshot.image_decodes);
904 self.component_state = snapshot.component_state;
905 self.stores = snapshot.stores;
906 self.native_collections = snapshot.native_collections;
907 self.native_documents = snapshot.native_documents;
908 self.actions = snapshot.actions;
909 self.component_event_handlers = snapshot.component_event_handlers;
910 self.component_incarnations = snapshot.component_incarnations;
911 self.next_component_incarnation = snapshot.next_component_incarnation;
912 self.dirty = snapshot.dirty;
913 self.pending_events = snapshot.pending_events;
914 self.pending_actions = snapshot.pending_actions;
915 self.pending_async = snapshot.pending_async;
916 self.pending_element_commands = snapshot.pending_element_commands;
917 self.locale = snapshot.locale;
918 self.theme = snapshot.theme;
919 self.component_styles = snapshot.component_styles;
920 self.component_style_generation = snapshot.component_style_generation;
921 self.motions = snapshot.motions;
922 self.effects = snapshot.effects;
923 self.signals = snapshot.signals;
924 self.element_refs = snapshot.element_refs;
925 self.geometry.restore(snapshot.geometry);
926 self.pointer_capture.restore(snapshot.pointer_capture);
927 self.presentations
928 .retain(|view, _| snapshot.presentations.contains_key(view));
929 for (view, presentation) in snapshot.presentations {
930 let state = self.presentations.entry(view).or_default();
931 state.geometry.restore(presentation.geometry);
932 state.pointer_capture.restore(presentation.pointer_capture);
933 }
934 self.budgets = snapshot.budgets;
935 self.virtual_requests.restore(snapshot.virtual_requests);
936 self.motion_values = snapshot.motion_values;
937 self.motion_ghosts = snapshot.motion_ghosts;
938 self.windows = snapshot.windows;
939 self.responsive = snapshot.responsive;
940 self.environment_dependencies = snapshot.environment_dependencies;
941 self.repaint_windows = snapshot.repaint_windows;
942 self.transaction_depth = snapshot.transaction_depth;
943 decode_result.map_err(UiTransactionError::from)
944 }
945
946 pub fn begin_transaction(&mut self) -> Result<UiStateSnapshot, UiTransactionError> {
955 let snapshot = self.snapshot()?;
956 let _ = self.assets.begin_transaction()?;
957 self.tasks.begin_transaction();
958 self.subscriptions.begin_transaction();
959 self.transaction_depth = self.transaction_depth.saturating_add(1);
960 Ok(snapshot)
961 }
962
963 pub fn commit_transaction(&mut self) -> Result<(), UiTransactionError> {
970 if self.transaction_depth == 0 {
971 return Err(UiTransactionError::Unbalanced {
972 expected: 1,
973 actual: 0,
974 });
975 }
976 self.assets.commit_transaction()?;
977 self.tasks.commit_transaction();
978 self.subscriptions.commit_transaction();
979 self.transaction_depth -= 1;
980 Ok(())
981 }
982
983 pub fn rollback_transaction(
990 &mut self,
991 snapshot: UiStateSnapshot,
992 ) -> Result<(), UiTransactionError> {
993 self.restore(snapshot)
994 }
995}
996
997#[derive(Debug, Default)]
998struct ViewPresentationState {
999 geometry: crate::GeometryRegistry,
1000 pointer_capture: crate::PointerCaptureRegistry,
1001}
1002
1003impl ViewPresentationState {
1004 fn snapshot(&self) -> ViewPresentationSnapshot {
1005 ViewPresentationSnapshot {
1006 geometry: self.geometry.snapshot(),
1007 pointer_capture: self.pointer_capture.snapshot(),
1008 }
1009 }
1010}
1011
1012#[derive(Clone, Debug)]
1013struct ViewPresentationSnapshot {
1014 geometry: crate::geometry::GeometrySnapshot,
1015 pointer_capture: BTreeMap<u64, crate::NodeId>,
1016}
1017
1018#[derive(Debug, Error)]
1019pub enum UiTransactionError {
1020 #[error("transaction snapshot belongs to another UiRuntimeState")]
1021 ForeignSnapshot,
1022 #[error("unbalanced runtime transaction depth: expected {expected}, got {actual}")]
1023 Unbalanced { expected: usize, actual: usize },
1024 #[error(transparent)]
1025 Asset(#[from] AssetError),
1026}
1027
1028pub struct UiStateSnapshot {
1029 runtime_id: RuntimeStateId,
1030 transaction_depth: usize,
1031 component_state: StateStore,
1032 stores: StoreRegistry,
1033 native_collections: crate::NativeCollectionRegistry,
1034 native_documents: crate::NativeTextDocumentRegistry,
1035 actions: ActionRegistry,
1036 component_event_handlers: BTreeMap<(ComponentInstancePath, String), ScriptCallback>,
1037 component_incarnations: BTreeMap<ComponentInstancePath, ComponentIncarnation>,
1038 next_component_incarnation: u64,
1039 dirty: BTreeSet<ComponentInstancePath>,
1040 pending_events: Vec<PendingEvent>,
1041 pending_actions: Vec<ActionInvocation>,
1042 pending_async: Vec<crate::AsyncDelivery>,
1043 pending_element_commands: Vec<crate::element_ref::ElementCommand>,
1044 locale: Option<LocaleManager>,
1045 theme: Option<ThemeManager>,
1046 component_styles: crate::ComponentStyleSheet,
1047 component_style_generation: u64,
1048 motions: MotionRuntime,
1049 effects: crate::EffectRegistry,
1050 signals: crate::SignalRegistry,
1051 element_refs: crate::ElementRefRegistry,
1052 geometry: crate::geometry::GeometrySnapshot,
1053 pointer_capture: BTreeMap<u64, crate::NodeId>,
1054 presentations: BTreeMap<String, ViewPresentationSnapshot>,
1055 budgets: crate::RuntimeBudgets,
1056 virtual_requests: crate::virtual_list::VirtualRequestSnapshot,
1057 motion_values: BTreeMap<MotionKey, f64>,
1058 motion_ghosts: Vec<crate::motion::MotionGhost>,
1059 windows: WindowCommandRegistry,
1060 responsive: ResponsiveRuntime,
1061 environment_dependencies: crate::environment_dependency::EnvironmentDependencyRegistry,
1062 repaint_windows: BTreeSet<String>,
1063 tasks: crate::async_runtime::TaskRegistrySnapshot,
1064 subscriptions: crate::async_runtime::SubscriptionRegistrySnapshot,
1065 timers: crate::TimerRegistry,
1066 image_decodes: crate::asset::ImageDecodeSnapshot,
1067}
1068
1069impl Clone for UiStateSnapshot {
1070 fn clone(&self) -> Self {
1071 Self {
1072 runtime_id: self.runtime_id,
1073 transaction_depth: self.transaction_depth,
1074 component_state: self.component_state.clone(),
1075 stores: self.stores.clone(),
1076 native_collections: self.native_collections.clone(),
1077 native_documents: self.native_documents.clone(),
1078 actions: self.actions.clone(),
1079 component_event_handlers: self.component_event_handlers.clone(),
1080 component_incarnations: self.component_incarnations.clone(),
1081 next_component_incarnation: self.next_component_incarnation,
1082 dirty: self.dirty.clone(),
1083 pending_events: self.pending_events.clone(),
1084 pending_actions: self.pending_actions.clone(),
1085 pending_async: self.pending_async.clone(),
1086 pending_element_commands: self.pending_element_commands.clone(),
1087 locale: self.locale.clone(),
1088 theme: self.theme.clone(),
1089 component_styles: self.component_styles.clone(),
1090 component_style_generation: self.component_style_generation,
1091 motions: self.motions.transaction_snapshot(),
1092 effects: self.effects.clone(),
1093 signals: self.signals.clone(),
1094 element_refs: self.element_refs.clone(),
1095 geometry: self.geometry.clone(),
1096 pointer_capture: self.pointer_capture.clone(),
1097 presentations: self.presentations.clone(),
1098 budgets: self.budgets.clone(),
1099 virtual_requests: self.virtual_requests.clone(),
1100 motion_values: self.motion_values.clone(),
1101 motion_ghosts: self.motion_ghosts.clone(),
1102 windows: self.windows.clone(),
1103 responsive: self.responsive.clone(),
1104 environment_dependencies: self.environment_dependencies.clone(),
1105 repaint_windows: self.repaint_windows.clone(),
1106 tasks: self.tasks.clone(),
1107 subscriptions: self.subscriptions.clone(),
1108 timers: self.timers.clone(),
1109 image_decodes: self.image_decodes.clone(),
1110 }
1111 }
1112}
1113
1114impl UiStateSnapshot {
1115 pub(crate) const fn component_state(&self) -> &StateStore {
1116 &self.component_state
1117 }
1118}
1119
1120#[derive(Clone, Debug)]
1121pub struct PendingEvent {
1122 pub target: ComponentInstancePath,
1123 pub event: UiEvent,
1124 pub origin: crate::InvocationOrigin,
1127}
1128
1129#[derive(Clone, Debug, Default)]
1130pub struct UiMutationBatch {
1131 pub dirty: BTreeSet<ComponentInstancePath>,
1132 pub events: Vec<PendingEvent>,
1133 pub actions: Vec<ActionInvocation>,
1134}
1135
1136#[derive(Clone, Debug)]
1137pub struct UiContext {
1138 runtime: Rc<RefCell<UiRuntimeState>>,
1139 component: ComponentInstancePath,
1140 incarnation: ComponentIncarnation,
1141 callback_component: ComponentInstancePath,
1142 callback_incarnation: ComponentIncarnation,
1143 callback_events: BTreeMap<String, EventSchema>,
1144 read_dependency: crate::read_dependency::ReadDependency,
1145 window: Option<String>,
1146 view: Option<String>,
1147 phase: ExecutionPhase,
1148 events: BTreeMap<String, EventSchema>,
1149 generation: ScriptGeneration,
1150 native_context: Option<crate::invocation::ScriptInvocationContext>,
1151 async_scope: Option<AsyncScope>,
1152 component_style: Option<crate::Style>,
1153 component_part_styles: BTreeMap<String, crate::Style>,
1154 global_component_part_styles: BTreeMap<String, crate::Style>,
1155 non_reusable_render_reads: Rc<RefCell<BTreeSet<ComponentInstancePath>>>,
1156 event_target: Option<crate::GeometryBounds>,
1157}
1158
1159impl UiContext {
1160 #[must_use]
1161 pub fn new(
1162 runtime: Rc<RefCell<UiRuntimeState>>,
1163 component: ComponentInstancePath,
1164 window: Option<String>,
1165 phase: ExecutionPhase,
1166 events: BTreeMap<String, EventSchema>,
1167 ) -> Self {
1168 if let Some(window) = window.as_deref()
1169 && let Ok(mut runtime) = runtime.try_borrow_mut()
1170 {
1171 runtime.ensure_presentation(window);
1172 }
1173 let incarnation = runtime
1174 .try_borrow_mut()
1175 .map(|mut runtime| runtime.ensure_component_incarnation(&component))
1176 .unwrap_or_default();
1177 let read_dependency = crate::read_dependency::ReadDependency::component(&component);
1178 let context = Self {
1179 runtime,
1180 component: component.clone(),
1181 incarnation,
1182 callback_component: component,
1183 read_dependency,
1184 callback_incarnation: incarnation,
1185 callback_events: events.clone(),
1186 window,
1187 view: None,
1188 phase,
1189 events,
1190 generation: ScriptGeneration::default(),
1191 native_context: None,
1192 async_scope: None,
1193 component_style: None,
1194 component_part_styles: BTreeMap::new(),
1195 global_component_part_styles: BTreeMap::new(),
1196 non_reusable_render_reads: Rc::new(RefCell::new(BTreeSet::new())),
1197 event_target: None,
1198 };
1199 if phase == ExecutionPhase::Render
1200 && let Ok(mut runtime) = context.runtime.try_borrow_mut()
1201 {
1202 runtime.reset_component_readers(&context.component);
1203 }
1204 context
1205 }
1206
1207 #[must_use]
1208 pub fn with_generation(mut self, generation: ScriptGeneration) -> Self {
1209 self.generation = generation;
1210 self
1211 }
1212
1213 #[must_use]
1214 pub fn with_view_id(mut self, view: impl Into<String>) -> Self {
1215 let view = view.into();
1216 if let Ok(mut runtime) = self.runtime.try_borrow_mut() {
1217 runtime.ensure_presentation(&view);
1218 }
1219 self.view = Some(view);
1220 self
1221 }
1222
1223 #[must_use]
1224 pub(crate) fn with_optional_view_id(mut self, view: Option<String>) -> Self {
1225 if let Some(view) = view.as_deref()
1226 && let Ok(mut runtime) = self.runtime.try_borrow_mut()
1227 {
1228 runtime.ensure_presentation(view);
1229 }
1230 self.view = view;
1231 self
1232 }
1233
1234 pub(crate) fn for_component(
1235 &self,
1236 component: ComponentInstancePath,
1237 events: BTreeMap<String, EventSchema>,
1238 ) -> Self {
1239 let incarnation = self
1240 .runtime
1241 .try_borrow_mut()
1242 .map(|mut runtime| runtime.ensure_component_incarnation(&component))
1243 .unwrap_or_default();
1244 let read_dependency = crate::read_dependency::ReadDependency::component(&component);
1245 let context = Self {
1246 runtime: Rc::clone(&self.runtime),
1247 component: component.clone(),
1248 incarnation,
1249 callback_component: component,
1250 read_dependency,
1251 callback_incarnation: incarnation,
1252 callback_events: events.clone(),
1253 window: self.window.clone(),
1254 view: self.view.clone(),
1255 phase: self.phase,
1256 events,
1257 generation: self.generation,
1258 native_context: self.native_context.clone(),
1259 async_scope: self.async_scope.clone(),
1260 component_style: self.component_style.clone(),
1261 component_part_styles: self.component_part_styles.clone(),
1262 global_component_part_styles: self.global_component_part_styles.clone(),
1263 non_reusable_render_reads: Rc::clone(&self.non_reusable_render_reads),
1264 event_target: self.event_target,
1265 };
1266 if context.phase == ExecutionPhase::Render
1267 && let Ok(mut runtime) = context.runtime.try_borrow_mut()
1268 {
1269 runtime.reset_component_readers(&context.component);
1270 }
1271 context
1272 }
1273
1274 pub(crate) fn for_structural_scope(
1275 &self,
1276 component: ComponentInstancePath,
1277 events: BTreeMap<String, EventSchema>,
1278 ) -> Self {
1279 let mut context = self.for_component(component, events);
1282 context.callback_component = self.callback_component.clone();
1283 context.callback_incarnation = self.callback_incarnation;
1284 context.callback_events = self.callback_events.clone();
1285 context.read_dependency = self.read_dependency.clone();
1286 context
1287 }
1288
1289 pub(crate) fn for_virtual_item(
1290 &self,
1291 collection: &crate::VirtualCollectionId,
1292 key: &str,
1293 ) -> Self {
1294 let mut context = self.clone();
1295 context.read_dependency = crate::read_dependency::ReadDependency::virtual_item(
1296 &self.callback_component,
1297 collection,
1298 key,
1299 );
1300 if let Ok(mut runtime) = context.runtime.try_borrow_mut() {
1301 runtime
1302 .native_collections
1303 .reset_contribution(&context.read_dependency);
1304 runtime
1305 .native_documents
1306 .reset_contribution(&context.read_dependency);
1307 runtime.stores.reset_contribution(&context.read_dependency);
1308 runtime
1309 .environment_dependencies
1310 .reset_contribution(&context.read_dependency);
1311 runtime.reset_geometry_read_contribution(&context.read_dependency);
1312 }
1313 context
1314 }
1315
1316 pub(crate) fn with_native_context(
1317 mut self,
1318 native_context: Option<crate::invocation::ScriptInvocationContext>,
1319 ) -> Self {
1320 self.native_context = native_context;
1321 self
1322 }
1323
1324 pub(crate) const fn with_event_target(mut self, target: Option<crate::GeometryBounds>) -> Self {
1325 self.event_target = target;
1326 self
1327 }
1328
1329 pub(crate) fn with_async_scope(mut self, scope: AsyncScope) -> Self {
1330 self.async_scope = Some(scope);
1331 self
1332 }
1333
1334 pub(crate) fn with_component_styles(
1335 mut self,
1336 global_part_styles: BTreeMap<String, crate::Style>,
1337 style: Option<crate::Style>,
1338 part_styles: BTreeMap<String, crate::Style>,
1339 ) -> Self {
1340 self.global_component_part_styles = global_part_styles;
1341 self.component_style = style;
1342 self.component_part_styles = part_styles;
1343 self
1344 }
1345
1346 pub(crate) fn reset_non_reusable_render_reads(&self) {
1347 self.non_reusable_render_reads.borrow_mut().clear();
1348 }
1349
1350 pub(crate) fn component_render_is_reusable(&self) -> bool {
1351 !self
1352 .non_reusable_render_reads
1353 .borrow()
1354 .contains(&self.component)
1355 }
1356
1357 fn render_reader(&self) -> Option<&crate::read_dependency::ReadDependency> {
1360 (self.phase == ExecutionPhase::Render).then_some(&self.read_dependency)
1361 }
1362
1363 fn mark_non_reusable_render_read(&self) {
1364 if self.phase == ExecutionPhase::Render {
1365 self.non_reusable_render_reads
1366 .borrow_mut()
1367 .insert(self.callback_component.clone());
1368 }
1369 }
1370
1371 fn resolve_component_style(&self, part: &str, mut base: crate::Style) -> crate::Style {
1372 if let Some(style) = self.global_component_part_styles.get(part) {
1373 base = base.merged(style);
1374 }
1375 if part == "root"
1376 && let Some(style) = &self.component_style
1377 {
1378 base = base.merged(style);
1379 }
1380 if let Some(style) = self.component_part_styles.get(part) {
1381 base = base.merged(style);
1382 }
1383 base
1384 }
1385
1386 fn scoped_callback(&self, function: FnPtr) -> Result<ScriptCallback, UiContextError> {
1387 let mut callback = ScriptCallback::try_from_fn_ptr(function, self.generation)?;
1388 callback.bind_component_scope_if_unset(
1389 &self.callback_component,
1390 self.callback_incarnation,
1391 self.callback_events.clone(),
1392 );
1393 if let Some(context) = &self.native_context {
1394 callback.bind_native_context_if_unset(context.clone());
1395 }
1396 Ok(callback)
1397 }
1398
1399 #[must_use]
1400 pub fn runtime(&self) -> &Rc<RefCell<UiRuntimeState>> {
1401 &self.runtime
1402 }
1403
1404 pub(crate) fn component_path(&self) -> &ComponentInstancePath {
1405 &self.component
1406 }
1407
1408 pub(crate) const fn component_incarnation(&self) -> ComponentIncarnation {
1409 self.incarnation
1410 }
1411
1412 pub(crate) fn event_schemas(&self) -> &BTreeMap<String, EventSchema> {
1413 &self.events
1414 }
1415
1416 pub(crate) fn callback_component_path(&self) -> &ComponentInstancePath {
1417 &self.callback_component
1418 }
1419
1420 pub(crate) const fn callback_component_incarnation(&self) -> ComponentIncarnation {
1421 self.callback_incarnation
1422 }
1423
1424 pub(crate) fn callback_event_schemas(&self) -> &BTreeMap<String, EventSchema> {
1425 &self.callback_events
1426 }
1427
1428 pub(crate) fn native_context(&self) -> Option<&crate::invocation::ScriptInvocationContext> {
1429 self.native_context.as_ref()
1430 }
1431
1432 pub fn get_state(&self, field: &str) -> Result<UiValue, UiContextError> {
1439 self.runtime
1440 .try_borrow()
1441 .map_err(|_| UiContextError::Borrowed)?
1442 .component_state
1443 .get(&self.callback_component, field)
1444 .cloned()
1445 .ok_or_else(|| UiContextError::UnknownState {
1446 component: self.callback_component.clone(),
1447 field: field.to_owned(),
1448 })
1449 }
1450
1451 pub fn get_state_path(
1461 &self,
1462 field: &str,
1463 path: &UiValuePath,
1464 ) -> Result<UiValue, UiContextError> {
1465 Ok(self.get_state(field)?.get_path(path)?.clone())
1466 }
1467
1468 pub fn set_state(&self, field: &str, value: Dynamic) -> Result<(), UiContextError> {
1475 self.require_mutation()?;
1476 let value = UiValue::from_dynamic(value)?;
1477 let mut runtime = self
1478 .runtime
1479 .try_borrow_mut()
1480 .map_err(|_| UiContextError::Borrowed)?;
1481 let sensitive = runtime.component_state.is_sensitive(&self.component, field);
1482 let changed = runtime
1483 .component_state
1484 .set(&self.component, field, value.clone())?;
1485 if changed {
1486 runtime.dirty.insert(self.component.clone());
1487 }
1488 runtime.traces.push(
1489 crate::RuntimeTraceKind::State,
1490 self.component.to_string(),
1491 format!("set {field}"),
1492 Some(value),
1493 sensitive,
1494 );
1495 Ok(())
1496 }
1497
1498 pub fn set_state_path(
1505 &self,
1506 field: &str,
1507 path: &UiValuePath,
1508 value: Dynamic,
1509 ) -> Result<(), UiContextError> {
1510 self.require_mutation()?;
1511 let value = UiValue::from_dynamic(value)?;
1512 let mut runtime = self
1513 .runtime
1514 .try_borrow_mut()
1515 .map_err(|_| UiContextError::Borrowed)?;
1516 let mut root = runtime
1517 .component_state
1518 .get(&self.component, field)
1519 .cloned()
1520 .ok_or_else(|| UiContextError::UnknownState {
1521 component: self.component.clone(),
1522 field: field.to_owned(),
1523 })?;
1524 root.set_path(path, value.clone())?;
1525 let sensitive = runtime.component_state.is_sensitive(&self.component, field);
1526 if runtime.component_state.set(&self.component, field, root)? {
1527 runtime.dirty.insert(self.component.clone());
1528 }
1529 runtime.traces.push(
1530 crate::RuntimeTraceKind::State,
1531 self.component.to_string(),
1532 format!("set nested {field}"),
1533 Some(value),
1534 sensitive,
1535 );
1536 Ok(())
1537 }
1538
1539 pub fn get_signal(&self, signal: &crate::NativeSignal) -> Result<Dynamic, UiContextError> {
1545 self.mark_non_reusable_render_read();
1546 Ok(self
1547 .runtime
1548 .try_borrow()
1549 .map_err(|_| UiContextError::Borrowed)?
1550 .signals
1551 .read(signal)?
1552 .into_dynamic())
1553 }
1554
1555 pub fn set_signal(
1562 &self,
1563 signal: &crate::NativeSignal,
1564 value: Dynamic,
1565 ) -> Result<(), UiContextError> {
1566 self.require_mutation()?;
1567 let value = crate::SignalValue::from_dynamic_for_kind(value, signal.id().kind())?;
1568 let mut runtime = self
1569 .runtime
1570 .try_borrow_mut()
1571 .map_err(|_| UiContextError::Borrowed)?;
1572 let changed =
1573 runtime
1574 .signals
1575 .write_from(signal, value.clone(), crate::SignalWriter::Script)?;
1576 runtime.traces.push(
1577 crate::RuntimeTraceKind::Signal,
1578 signal.id().component().to_string(),
1579 format!(
1580 "set {} ({}){}",
1581 signal.id().key(),
1582 signal.id().kind().as_str(),
1583 if changed { "" } else { " unchanged" }
1584 ),
1585 None,
1586 false,
1587 );
1588 Ok(())
1589 }
1590
1591 pub fn get_signal_by_key(&self, key: &str) -> Result<Dynamic, UiContextError> {
1597 self.mark_non_reusable_render_read();
1598 let runtime = self
1599 .runtime
1600 .try_borrow()
1601 .map_err(|_| UiContextError::Borrowed)?;
1602 let signal = runtime.signals.resolve(&self.component, key)?;
1603 Ok(runtime.signals.read(&signal)?.into_dynamic())
1604 }
1605
1606 pub(crate) fn parent_optional_float_signal_ref(
1616 &self,
1617 key: &str,
1618 pending: &[crate::SignalId],
1619 ) -> Result<crate::NativeSignal, UiContextError> {
1620 let runtime = self
1621 .runtime
1622 .try_borrow()
1623 .map_err(|_| UiContextError::Borrowed)?;
1624 let mut scope = self.component.parent();
1625 while let Some(component) = scope {
1626 let signal = pending
1627 .iter()
1628 .find(|id| id.component() == &component && id.key() == key)
1629 .cloned()
1630 .map(crate::NativeSignal::new)
1631 .or_else(|| runtime.signals.resolve(&component, key).ok());
1632 if let Some(signal) = signal {
1633 if signal.id().kind() == crate::SignalKind::OptionalFloat {
1634 return Ok(signal);
1635 }
1636 return Err(crate::SignalError::TypeMismatch {
1637 expected: crate::SignalKind::OptionalFloat,
1638 actual: signal.id().kind(),
1639 }
1640 .into());
1641 }
1642 scope = component.parent();
1643 }
1644 Err(crate::SignalError::UnknownKey {
1645 component: self.component.clone(),
1646 key: key.to_owned(),
1647 }
1648 .into())
1649 }
1650
1651 pub fn set_signal_by_key(&self, key: &str, value: Dynamic) -> Result<(), UiContextError> {
1657 self.require_mutation()?;
1658 let signal = self
1659 .runtime
1660 .try_borrow()
1661 .map_err(|_| UiContextError::Borrowed)?
1662 .signals
1663 .resolve(&self.component, key)?;
1664 self.set_signal(&signal, value)
1665 }
1666
1667 pub fn element_bounds(&self, reference: &crate::ElementRef) -> Result<UiValue, UiContextError> {
1676 let mut runtime = self
1677 .runtime
1678 .try_borrow_mut()
1679 .map_err(|_| UiContextError::Borrowed)?;
1680 let node = if self.phase == ExecutionPhase::Render {
1681 runtime
1682 .element_refs
1683 .resolve_and_track_geometry(reference, &self.read_dependency)
1684 } else {
1685 Some(runtime.element_refs.resolve(reference)?)
1686 };
1687 let Some(node) = node else {
1688 return Ok(UiValue::Null);
1689 };
1690 let geometry_registry =
1691 runtime.geometry_for(self.view.as_deref().or(self.window.as_deref()));
1692 drop(runtime);
1693 let geometry = if self.phase == ExecutionPhase::Render {
1694 geometry_registry.read_ref_tracked(reference.id(), node, &self.read_dependency)
1695 } else {
1696 geometry_registry.get(node)
1697 };
1698 let Some(geometry) = geometry else {
1699 return Ok(UiValue::Null);
1700 };
1701 Ok(UiValue::Map(BTreeMap::from([
1702 ("layout".to_owned(), geometry_bounds_value(geometry.layout)),
1703 ("visual".to_owned(), geometry_bounds_value(geometry.visual)),
1704 (
1705 "clip".to_owned(),
1706 geometry.clip.map_or(UiValue::Null, geometry_bounds_value),
1707 ),
1708 ])))
1709 }
1710
1711 pub fn element_bounds_by_key(&self, key: &str) -> Result<UiValue, UiContextError> {
1720 let reference = self
1721 .runtime
1722 .try_borrow()
1723 .map_err(|_| UiContextError::Borrowed)?
1724 .element_refs
1725 .resolve_key(&self.component, key)?;
1726 self.element_bounds(&reference)
1727 }
1728
1729 pub fn virtual_item_bounds(
1737 &self,
1738 collection: &str,
1739 index: usize,
1740 ) -> Result<UiValue, UiContextError> {
1741 let id = crate::VirtualCollectionId {
1742 component: self.component.clone(),
1743 key: collection.to_owned(),
1744 };
1745 let registry = self
1746 .runtime
1747 .try_borrow()
1748 .map_err(|_| UiContextError::Borrowed)?
1749 .virtual_requests
1750 .clone();
1751 Ok(registry
1752 .item_bounds(&id, index)
1753 .map_or(UiValue::Null, crate::GeometryBounds::into_value))
1754 }
1755
1756 pub fn event_target_bounds(&self) -> Result<UiValue, UiContextError> {
1767 if self.phase != ExecutionPhase::Event {
1768 return Err(UiContextError::EventTargetOutsideEvent);
1769 }
1770 Ok(self
1771 .event_target
1772 .map_or(UiValue::Null, crate::GeometryBounds::into_value))
1773 }
1774
1775 pub fn focus_element(&self, reference: &crate::ElementRef) -> Result<(), UiContextError> {
1781 self.require_mutation()?;
1782 let window = self.window.clone().ok_or(UiContextError::MissingWindow)?;
1783 let mut runtime = self
1784 .runtime
1785 .try_borrow_mut()
1786 .map_err(|_| UiContextError::Borrowed)?;
1787 let node = runtime.element_refs.resolve(reference)?;
1788 runtime
1789 .pending_element_commands
1790 .push(crate::element_ref::ElementCommand::Focus { window, node });
1791 Ok(())
1792 }
1793
1794 pub fn focus_element_by_key(&self, key: &str) -> Result<(), UiContextError> {
1800 let reference = self
1801 .runtime
1802 .try_borrow()
1803 .map_err(|_| UiContextError::Borrowed)?
1804 .element_refs
1805 .resolve_key(&self.component, key)?;
1806 self.focus_element(&reference)
1807 }
1808
1809 pub fn scroll_element_to(
1816 &self,
1817 reference: &crate::ElementRef,
1818 x: f64,
1819 y: f64,
1820 ) -> Result<(), UiContextError> {
1821 self.require_mutation()?;
1822 if !x.is_finite() || !y.is_finite() || x < 0.0 || y < 0.0 {
1823 return Err(UiContextError::InvalidScrollOffset { x, y });
1824 }
1825 let window = self.window.clone().ok_or(UiContextError::MissingWindow)?;
1826 let mut runtime = self
1827 .runtime
1828 .try_borrow_mut()
1829 .map_err(|_| UiContextError::Borrowed)?;
1830 let node = runtime.element_refs.resolve(reference)?;
1831 runtime
1832 .pending_element_commands
1833 .push(crate::element_ref::ElementCommand::ScrollTo { window, node, x, y });
1834 Ok(())
1835 }
1836
1837 pub fn scroll_element_to_by_key(
1843 &self,
1844 key: &str,
1845 x: f64,
1846 y: f64,
1847 ) -> Result<(), UiContextError> {
1848 let reference = self
1849 .runtime
1850 .try_borrow()
1851 .map_err(|_| UiContextError::Borrowed)?
1852 .element_refs
1853 .resolve_key(&self.component, key)?;
1854 self.scroll_element_to(&reference, x, y)
1855 }
1856
1857 pub fn scroll_element_into_view(
1863 &self,
1864 reference: &crate::ElementRef,
1865 ) -> Result<(), UiContextError> {
1866 self.require_mutation()?;
1867 let window = self.window.clone().ok_or(UiContextError::MissingWindow)?;
1868 let mut runtime = self
1869 .runtime
1870 .try_borrow_mut()
1871 .map_err(|_| UiContextError::Borrowed)?;
1872 let node = runtime.element_refs.resolve(reference)?;
1873 runtime
1874 .pending_element_commands
1875 .push(crate::element_ref::ElementCommand::ScrollIntoView { window, node });
1876 Ok(())
1877 }
1878
1879 pub fn scroll_element_into_view_by_key(&self, key: &str) -> Result<(), UiContextError> {
1885 let reference = self
1886 .runtime
1887 .try_borrow()
1888 .map_err(|_| UiContextError::Borrowed)?
1889 .element_refs
1890 .resolve_key(&self.component, key)?;
1891 self.scroll_element_into_view(&reference)
1892 }
1893
1894 pub fn get_app_store(&self, store: &str, field: &str) -> Result<UiValue, UiContextError> {
1900 let mut runtime = self
1901 .runtime
1902 .try_borrow_mut()
1903 .map_err(|_| UiContextError::Borrowed)?;
1904 Ok(runtime
1905 .stores
1906 .read_dependency(self.render_reader(), &StoreId::app(store), field)?)
1907 }
1908
1909 pub fn get_native_collection(
1918 &self,
1919 name: &str,
1920 ) -> Result<crate::NativeCollection, UiContextError> {
1921 let mut runtime = self
1922 .runtime
1923 .try_borrow_mut()
1924 .map_err(|_| UiContextError::Borrowed)?;
1925 Ok(runtime
1926 .native_collections
1927 .read_dependency(self.render_reader(), name, true)?)
1928 }
1929
1930 pub fn get_native_text_document(
1936 &self,
1937 name: &str,
1938 ) -> Result<crate::NativeTextDocument, UiContextError> {
1939 let mut runtime = self
1940 .runtime
1941 .try_borrow_mut()
1942 .map_err(|_| UiContextError::Borrowed)?;
1943 Ok(runtime
1944 .native_documents
1945 .read_dependency(self.render_reader(), name)?)
1946 }
1947
1948 #[cfg(feature = "charts")]
1956 pub fn get_native_chart_data(
1957 &self,
1958 name: &str,
1959 ) -> Result<crate::NativeChartData, UiContextError> {
1960 self.runtime
1961 .try_borrow()
1962 .map_err(|_| UiContextError::Borrowed)?
1963 .native_chart_data
1964 .get(name)
1965 .cloned()
1966 .ok_or_else(|| UiContextError::UnknownChartData(name.to_owned()))
1967 }
1968
1969 pub fn get_app_store_path(
1975 &self,
1976 store: &str,
1977 field: &str,
1978 path: &UiValuePath,
1979 ) -> Result<UiValue, UiContextError> {
1980 let mut runtime = self
1981 .runtime
1982 .try_borrow_mut()
1983 .map_err(|_| UiContextError::Borrowed)?;
1984 Ok(runtime.stores.read_path_dependency(
1985 self.render_reader(),
1986 &StoreId::app(store),
1987 field,
1988 path,
1989 )?)
1990 }
1991
1992 pub fn set_app_store(
1998 &self,
1999 store: &str,
2000 field: &str,
2001 value: Dynamic,
2002 ) -> Result<(), UiContextError> {
2003 self.require_mutation()?;
2004 let value = UiValue::from_dynamic(value)?;
2005 let mut runtime = self
2006 .runtime
2007 .try_borrow_mut()
2008 .map_err(|_| UiContextError::Borrowed)?;
2009 let id = StoreId::app(store);
2010 let sensitive = runtime.stores.is_sensitive(&id, field);
2011 let invalidated = runtime.stores.write(&id, field, value.clone())?;
2012 runtime.dirty.extend(invalidated);
2013 runtime.traces.push(
2014 crate::RuntimeTraceKind::Store,
2015 format!("app:{store}"),
2016 format!("set {field}"),
2017 Some(value),
2018 sensitive,
2019 );
2020 Ok(())
2021 }
2022
2023 pub fn set_app_store_path(
2029 &self,
2030 store: &str,
2031 field: &str,
2032 path: &UiValuePath,
2033 value: Dynamic,
2034 ) -> Result<(), UiContextError> {
2035 self.require_mutation()?;
2036 let value = UiValue::from_dynamic(value)?;
2037 let mut runtime = self
2038 .runtime
2039 .try_borrow_mut()
2040 .map_err(|_| UiContextError::Borrowed)?;
2041 let id = StoreId::app(store);
2042 let sensitive = runtime.stores.is_sensitive(&id, field);
2043 let invalidated = runtime.stores.write_path(&id, field, path, value.clone())?;
2044 runtime.dirty.extend(invalidated);
2045 runtime.traces.push(
2046 crate::RuntimeTraceKind::Store,
2047 format!("app:{store}"),
2048 format!("set nested {field}"),
2049 Some(value),
2050 sensitive,
2051 );
2052 Ok(())
2053 }
2054
2055 pub fn get_window_store(&self, store: &str, field: &str) -> Result<UiValue, UiContextError> {
2062 let window = self.window.as_ref().ok_or(UiContextError::MissingWindow)?;
2063 let mut runtime = self
2064 .runtime
2065 .try_borrow_mut()
2066 .map_err(|_| UiContextError::Borrowed)?;
2067 Ok(runtime.stores.read_dependency(
2068 self.render_reader(),
2069 &StoreId::window(window, store),
2070 field,
2071 )?)
2072 }
2073
2074 pub fn get_window_store_path(
2080 &self,
2081 store: &str,
2082 field: &str,
2083 path: &UiValuePath,
2084 ) -> Result<UiValue, UiContextError> {
2085 let window = self.window.as_ref().ok_or(UiContextError::MissingWindow)?;
2086 let mut runtime = self
2087 .runtime
2088 .try_borrow_mut()
2089 .map_err(|_| UiContextError::Borrowed)?;
2090 Ok(runtime.stores.read_path_dependency(
2091 self.render_reader(),
2092 &StoreId::window(window, store),
2093 field,
2094 path,
2095 )?)
2096 }
2097
2098 pub fn set_window_store(
2104 &self,
2105 store: &str,
2106 field: &str,
2107 value: Dynamic,
2108 ) -> Result<(), UiContextError> {
2109 self.require_mutation()?;
2110 let window = self.window.as_ref().ok_or(UiContextError::MissingWindow)?;
2111 let value = UiValue::from_dynamic(value)?;
2112 let mut runtime = self
2113 .runtime
2114 .try_borrow_mut()
2115 .map_err(|_| UiContextError::Borrowed)?;
2116 let id = StoreId::window(window, store);
2117 let sensitive = runtime.stores.is_sensitive(&id, field);
2118 let invalidated = runtime.stores.write(&id, field, value.clone())?;
2119 runtime.dirty.extend(invalidated);
2120 runtime.traces.push(
2121 crate::RuntimeTraceKind::Store,
2122 format!("window:{window}:{store}"),
2123 format!("set {field}"),
2124 Some(value),
2125 sensitive,
2126 );
2127 Ok(())
2128 }
2129
2130 pub fn set_window_store_path(
2137 &self,
2138 store: &str,
2139 field: &str,
2140 path: &UiValuePath,
2141 value: Dynamic,
2142 ) -> Result<(), UiContextError> {
2143 self.require_mutation()?;
2144 let window = self.window.as_ref().ok_or(UiContextError::MissingWindow)?;
2145 let value = UiValue::from_dynamic(value)?;
2146 let mut runtime = self
2147 .runtime
2148 .try_borrow_mut()
2149 .map_err(|_| UiContextError::Borrowed)?;
2150 let id = StoreId::window(window, store);
2151 let sensitive = runtime.stores.is_sensitive(&id, field);
2152 let invalidated = runtime.stores.write_path(&id, field, path, value.clone())?;
2153 runtime.dirty.extend(invalidated);
2154 runtime.traces.push(
2155 crate::RuntimeTraceKind::Store,
2156 format!("window:{window}:{store}"),
2157 format!("set nested {field}"),
2158 Some(value),
2159 sensitive,
2160 );
2161 Ok(())
2162 }
2163
2164 pub fn emit(&self, event: &str, payload: Dynamic) -> Result<(), UiContextError> {
2170 self.require_mutation()?;
2171 let schema = self
2172 .events
2173 .get(event)
2174 .ok_or_else(|| UiContextError::UnknownEvent(event.to_owned()))?;
2175 schema
2176 .payload
2177 .validate(&payload)
2178 .map_err(UiContextError::InvalidEvent)?;
2179 let payload = UiValue::from_dynamic(payload)?;
2180 let mut runtime = self
2181 .runtime
2182 .try_borrow_mut()
2183 .map_err(|_| UiContextError::Borrowed)?;
2184 let origin = runtime.origin.clone();
2185 runtime.pending_events.push(PendingEvent {
2186 target: self.component.clone(),
2187 event: UiEvent {
2188 name: event.to_owned(),
2189 payload,
2190 },
2191 origin,
2192 });
2193 runtime.traces.push(
2194 crate::RuntimeTraceKind::Event,
2195 self.component.to_string(),
2196 format!("emit {event}"),
2197 None,
2198 true,
2199 );
2200 Ok(())
2201 }
2202
2203 pub fn dispatch_action(&self, action: &str, payload: Dynamic) -> Result<(), UiContextError> {
2209 self.require_mutation()?;
2210 let id = ActionId::parse(action)?;
2211 let payload = UiValue::from_dynamic(payload)?;
2212 let mut runtime = self
2213 .runtime
2214 .try_borrow_mut()
2215 .map_err(|_| UiContextError::Borrowed)?;
2216 let mut invocation = runtime.actions.dispatch(&id, payload)?;
2217 invocation.origin = runtime.origin.clone();
2218 runtime.pending_actions.push(invocation);
2219 runtime.traces.push(
2220 crate::RuntimeTraceKind::Action,
2221 self.component.to_string(),
2222 format!("dispatch {action}"),
2223 None,
2224 true,
2225 );
2226 Ok(())
2227 }
2228
2229 pub fn register_action(&self, action: &str, callback: FnPtr) -> Result<(), UiContextError> {
2235 self.require_mutation()?;
2236 let id = ActionId::parse(action)?;
2237 let callback = self.scoped_callback(callback)?;
2238 self.runtime
2239 .try_borrow_mut()
2240 .map_err(|_| UiContextError::Borrowed)?
2241 .actions
2242 .register_or_replace(id, callback);
2243 Ok(())
2244 }
2245
2246 pub fn set_action_enabled(&self, action: &str, enabled: bool) -> Result<(), UiContextError> {
2252 self.require_mutation()?;
2253 let id = ActionId::parse(action)?;
2254 self.runtime
2255 .try_borrow_mut()
2256 .map_err(|_| UiContextError::Borrowed)?
2257 .actions
2258 .set_enabled(&id, enabled)?;
2259 Ok(())
2260 }
2261
2262 pub fn action_shortcut(
2269 &self,
2270 action: &str,
2271 ) -> Result<Option<crate::ActionShortcut>, UiContextError> {
2272 let id = ActionId::parse(action)?;
2273 let runtime = self
2274 .runtime
2275 .try_borrow()
2276 .map_err(|_| UiContextError::Borrowed)?;
2277 Ok(runtime
2278 .key_bindings
2279 .iter()
2280 .find(|binding| binding.action == id)
2281 .map(crate::KeyBindingSpec::shortcut))
2282 }
2283
2284 pub fn action_enabled(&self, action: &str) -> Result<bool, UiContextError> {
2290 let id = ActionId::parse(action)?;
2291 Ok(self
2292 .runtime
2293 .try_borrow()
2294 .map_err(|_| UiContextError::Borrowed)?
2295 .actions
2296 .is_enabled(&id)
2297 .unwrap_or(false))
2298 }
2299
2300 pub fn actions(&self) -> Result<Vec<UiValue>, UiContextError> {
2307 let runtime = self
2308 .runtime
2309 .try_borrow()
2310 .map_err(|_| UiContextError::Borrowed)?;
2311 Ok(runtime
2312 .actions
2313 .ids()
2314 .map(|id| {
2315 let shortcut = runtime
2316 .key_bindings
2317 .iter()
2318 .find(|binding| &binding.action == id)
2319 .map_or(UiValue::Null, |binding| binding.shortcut().to_ui_value());
2320 UiValue::Map(BTreeMap::from([
2321 ("id".to_owned(), UiValue::String(id.as_str().to_owned())),
2322 (
2323 "enabled".to_owned(),
2324 UiValue::Bool(runtime.actions.is_enabled(id).unwrap_or(false)),
2325 ),
2326 ("shortcut".to_owned(), shortcut),
2327 ]))
2328 })
2329 .collect())
2330 }
2331
2332 pub fn call_capability(
2338 &self,
2339 capability: &str,
2340 method: &str,
2341 input: Dynamic,
2342 ) -> Result<UiValue, UiContextError> {
2343 self.require_mutation()?;
2344 let id = CapabilityId::parse(capability)?;
2345 let input = UiValue::from_dynamic(input)?;
2346 let mut runtime = self
2347 .runtime
2348 .try_borrow_mut()
2349 .map_err(|_| UiContextError::Borrowed)?;
2350 let context = self.invocation_context(&runtime);
2351 let output = runtime
2352 .capabilities
2353 .call_with(&context, &id, method, input)?;
2354 runtime.traces.push(
2355 crate::RuntimeTraceKind::Capability,
2356 self.component.to_string(),
2357 format!("call {capability}.{method}"),
2358 None,
2359 true,
2360 );
2361 Ok(output)
2362 }
2363
2364 fn invocation_context(&self, runtime: &UiRuntimeState) -> crate::InvocationContext {
2366 crate::InvocationContext {
2367 origin: runtime.origin.clone(),
2368 view_id: self.view.clone(),
2369 component: self.component.clone(),
2370 }
2371 }
2372
2373 fn read_locale<T>(
2374 &self,
2375 read: impl FnOnce(
2376 &LocaleManager,
2377 Option<&str>,
2378 &ComponentInstancePath,
2379 ) -> Result<T, LocaleError>,
2380 ) -> Result<T, UiContextError> {
2381 let mut runtime = self
2382 .runtime
2383 .try_borrow_mut()
2384 .map_err(|_| UiContextError::Borrowed)?;
2385 if let Some(reader) = self.render_reader() {
2386 runtime
2387 .environment_dependencies
2388 .track_locale(self.window.as_deref(), reader);
2389 }
2390 let locale = runtime
2391 .locale
2392 .as_ref()
2393 .ok_or(UiContextError::LocaleUnavailable)?;
2394 Ok(read(locale, self.window.as_deref(), &self.component)?)
2395 }
2396
2397 pub fn text(&self, key: &str) -> Result<String, UiContextError> {
2403 self.read_locale(|locale, window, component| locale.text(window, Some(component), key))
2404 }
2405
2406 pub fn text_direction(&self) -> Result<String, UiContextError> {
2412 match self
2413 .read_locale(|locale, window, component| locale.direction(window, Some(component)))?
2414 {
2415 TextDirection::LeftToRight => Ok("ltr".to_owned()),
2416 TextDirection::RightToLeft => Ok("rtl".to_owned()),
2417 }
2418 }
2419
2420 pub fn today(&self) -> Result<String, UiContextError> {
2428 Ok(self
2429 .runtime
2430 .try_borrow()
2431 .map_err(|_| UiContextError::Borrowed)?
2432 .calendar_clock
2433 .today()
2434 .to_iso())
2435 }
2436
2437 pub fn format_date(&self, iso_date: &str, style: &str) -> Result<String, UiContextError> {
2443 let style = DateStyle::parse(style)?;
2444 self.read_locale(|locale, window, component| {
2445 locale.format_date(window, Some(component), iso_date, style)
2446 })
2447 }
2448
2449 pub fn format_month_year(&self, iso_date: &str) -> Result<String, UiContextError> {
2455 self.read_locale(|locale, window, component| {
2456 locale.format_month_year(window, Some(component), iso_date)
2457 })
2458 }
2459
2460 pub fn calendar_metadata(&self) -> Result<Map, UiContextError> {
2466 let calendar = self.read_locale(|locale, window, component| {
2467 locale.calendar(window, Some(component)).cloned()
2468 })?;
2469 let dynamic = rhai::serde::to_dynamic(calendar)
2470 .map_err(|error| LocaleError::Decode(error.to_string()))?;
2471 Ok(dynamic.cast::<Map>())
2472 }
2473
2474 pub fn number_metadata(&self) -> Result<Map, UiContextError> {
2480 let number = self.read_locale(|locale, window, component| {
2481 locale.number(window, Some(component)).cloned()
2482 })?;
2483 let dynamic = rhai::serde::to_dynamic(number)
2484 .map_err(|error| LocaleError::Decode(error.to_string()))?;
2485 Ok(dynamic.cast::<Map>())
2486 }
2487
2488 pub fn format_integer(
2494 &self,
2495 value: INT,
2496 options: NumberFormatOptions,
2497 ) -> Result<String, UiContextError> {
2498 self.read_locale(|locale, window, component| {
2499 locale.format_integer(window, Some(component), value, options)
2500 })
2501 }
2502
2503 pub fn format_number(
2509 &self,
2510 value: FLOAT,
2511 options: NumberFormatOptions,
2512 ) -> Result<String, UiContextError> {
2513 self.read_locale(|locale, window, component| {
2514 locale.format_number(window, Some(component), value, options)
2515 })
2516 }
2517
2518 pub fn set_locale(&self, locale: &str) -> Result<(), UiContextError> {
2524 self.require_mutation()?;
2525 let mut runtime = self
2526 .runtime
2527 .try_borrow_mut()
2528 .map_err(|_| UiContextError::Borrowed)?;
2529 let changed = {
2530 let locales = runtime
2531 .locale
2532 .as_mut()
2533 .ok_or(UiContextError::LocaleUnavailable)?;
2534 let previous = locales.generation();
2535 locales.set_app(locale)?;
2536 locales.generation() != previous
2537 };
2538 if changed {
2539 let invalidated = runtime.environment_dependencies.invalidate_locale_app();
2540 runtime.dirty.extend(invalidated);
2541 runtime.mark_all_windows_repaint();
2542 }
2543 runtime.traces.push(
2544 crate::RuntimeTraceKind::Locale,
2545 self.component.to_string(),
2546 format!("select {locale}"),
2547 None,
2548 false,
2549 );
2550 Ok(())
2551 }
2552
2553 pub fn set_theme(&self, family: &str, variant: &str) -> Result<(), UiContextError> {
2559 self.set_theme_preference(
2560 ThemeTarget::App,
2561 ThemePreference::Fixed {
2562 selection: ThemeSelection::new(family, variant),
2563 },
2564 format!("select {family}/{variant}"),
2565 )
2566 }
2567
2568 pub fn set_window_theme(&self, family: &str, variant: &str) -> Result<(), UiContextError> {
2574 let window = self.window.clone().ok_or(UiContextError::MissingWindow)?;
2575 self.set_theme_preference(
2576 ThemeTarget::Window(window),
2577 ThemePreference::Fixed {
2578 selection: ThemeSelection::new(family, variant),
2579 },
2580 format!("select window {family}/{variant}"),
2581 )
2582 }
2583
2584 pub fn window_id(&self) -> Result<String, UiContextError> {
2590 self.window.clone().ok_or(UiContextError::MissingWindow)
2591 }
2592
2593 pub fn view_id(&self) -> Result<String, UiContextError> {
2600 self.view.clone().ok_or(UiContextError::MissingView)
2601 }
2602
2603 pub fn viewport_class(&self) -> Result<String, UiContextError> {
2609 let window = self.window.as_ref().ok_or(UiContextError::MissingWindow)?;
2610 let mut runtime = self
2611 .runtime
2612 .try_borrow_mut()
2613 .map_err(|_| UiContextError::Borrowed)?;
2614 if let Some(reader) = self.render_reader() {
2615 runtime
2616 .environment_dependencies
2617 .track_viewport(window, reader);
2618 }
2619 Ok(runtime.responsive.class(window).as_str().to_owned())
2620 }
2621
2622 pub fn open_window(&self, spec: ScriptWindowSpec) -> Result<(), UiContextError> {
2628 self.require_mutation()?;
2629 let window = self.window.as_ref().ok_or(UiContextError::MissingWindow)?;
2630 let id = spec.id.clone();
2631 let mut runtime = self
2632 .runtime
2633 .try_borrow_mut()
2634 .map_err(|_| UiContextError::Borrowed)?;
2635 runtime.windows.request_open_from(window, spec)?;
2636 runtime.traces.push(
2637 crate::RuntimeTraceKind::Window,
2638 self.component.to_string(),
2639 format!("open {id}"),
2640 None,
2641 false,
2642 );
2643 Ok(())
2644 }
2645
2646 pub fn focus_window(&self, id: &str) -> Result<(), UiContextError> {
2652 self.require_mutation()?;
2653 let window = self.window.as_ref().ok_or(UiContextError::MissingWindow)?;
2654 let mut runtime = self
2655 .runtime
2656 .try_borrow_mut()
2657 .map_err(|_| UiContextError::Borrowed)?;
2658 runtime.windows.request_focus_from(window, id)?;
2659 runtime.traces.push(
2660 crate::RuntimeTraceKind::Window,
2661 self.component.to_string(),
2662 format!("focus {id}"),
2663 None,
2664 false,
2665 );
2666 Ok(())
2667 }
2668
2669 pub fn close_window(&self, id: &str) -> Result<(), UiContextError> {
2675 self.require_mutation()?;
2676 let window = self.window.as_ref().ok_or(UiContextError::MissingWindow)?;
2677 let mut runtime = self
2678 .runtime
2679 .try_borrow_mut()
2680 .map_err(|_| UiContextError::Borrowed)?;
2681 runtime.windows.request_close_from(window, id)?;
2682 runtime.traces.push(
2683 crate::RuntimeTraceKind::Window,
2684 self.component.to_string(),
2685 format!("close {id}"),
2686 None,
2687 false,
2688 );
2689 Ok(())
2690 }
2691
2692 pub fn set_close_handler(&self, callback: FnPtr) -> Result<(), UiContextError> {
2699 self.require_mutation()?;
2700 let window = self.window.as_ref().ok_or(UiContextError::MissingWindow)?;
2701 let callback = self.scoped_callback(callback)?;
2702 self.runtime
2703 .try_borrow_mut()
2704 .map_err(|_| UiContextError::Borrowed)?
2705 .windows
2706 .set_close_handler(window, Some(callback))?;
2707 Ok(())
2708 }
2709
2710 pub fn clear_close_handler(&self) -> Result<(), UiContextError> {
2716 self.require_mutation()?;
2717 let window = self.window.as_ref().ok_or(UiContextError::MissingWindow)?;
2718 self.runtime
2719 .try_borrow_mut()
2720 .map_err(|_| UiContextError::Borrowed)?
2721 .windows
2722 .set_close_handler(window, None)?;
2723 Ok(())
2724 }
2725
2726 pub fn set_window_locale(&self, locale: &str) -> Result<(), UiContextError> {
2732 self.require_mutation()?;
2733 let window = self.window.clone().ok_or(UiContextError::MissingWindow)?;
2734 let mut runtime = self
2735 .runtime
2736 .try_borrow_mut()
2737 .map_err(|_| UiContextError::Borrowed)?;
2738 let changed = {
2739 let locales = runtime
2740 .locale
2741 .as_mut()
2742 .ok_or(UiContextError::LocaleUnavailable)?;
2743 let previous = locales.generation();
2744 locales.set_window(window.clone(), locale)?;
2745 locales.generation() != previous
2746 };
2747 if changed {
2748 let invalidated = runtime
2749 .environment_dependencies
2750 .invalidate_locale_window(&window);
2751 runtime.dirty.extend(invalidated);
2752 runtime.mark_window_repaint(window.clone());
2753 }
2754 runtime.traces.push(
2755 crate::RuntimeTraceKind::Locale,
2756 self.component.to_string(),
2757 format!("select window {window} locale {locale}"),
2758 None,
2759 false,
2760 );
2761 Ok(())
2762 }
2763
2764 pub fn set_local_locale(&self, locale: &str) -> Result<(), UiContextError> {
2770 self.require_mutation()?;
2771 let mut runtime = self
2772 .runtime
2773 .try_borrow_mut()
2774 .map_err(|_| UiContextError::Borrowed)?;
2775 let changed = {
2776 let locales = runtime
2777 .locale
2778 .as_mut()
2779 .ok_or(UiContextError::LocaleUnavailable)?;
2780 let previous = locales.generation();
2781 locales.set_scope(self.component.clone(), locale)?;
2782 locales.generation() != previous
2783 };
2784 if changed {
2785 let invalidated = runtime
2786 .environment_dependencies
2787 .invalidate_locale_scope(&self.component);
2788 runtime.dirty.extend(invalidated);
2789 if let Some(window) = &self.window {
2790 runtime.mark_window_repaint(window.clone());
2791 }
2792 }
2793 runtime.traces.push(
2794 crate::RuntimeTraceKind::Locale,
2795 self.component.to_string(),
2796 format!("select subtree locale {locale}"),
2797 None,
2798 false,
2799 );
2800 Ok(())
2801 }
2802
2803 pub fn set_local_theme(&self, family: &str, variant: &str) -> Result<(), UiContextError> {
2809 self.set_theme_preference(
2810 ThemeTarget::Local,
2811 ThemePreference::Fixed {
2812 selection: ThemeSelection::new(family, variant),
2813 },
2814 format!("select subtree {family}/{variant}"),
2815 )
2816 }
2817
2818 pub fn set_theme_system(&self, family: &str) -> Result<(), UiContextError> {
2824 self.set_theme_preference(
2825 ThemeTarget::App,
2826 ThemePreference::System {
2827 family: family.to_owned(),
2828 },
2829 format!("follow system family {family}"),
2830 )
2831 }
2832
2833 pub fn set_window_theme_system(&self, family: &str) -> Result<(), UiContextError> {
2839 let window = self.window.clone().ok_or(UiContextError::MissingWindow)?;
2840 self.set_theme_preference(
2841 ThemeTarget::Window(window),
2842 ThemePreference::System {
2843 family: family.to_owned(),
2844 },
2845 format!("follow window system family {family}"),
2846 )
2847 }
2848
2849 pub fn set_local_theme_system(&self, family: &str) -> Result<(), UiContextError> {
2855 self.set_theme_preference(
2856 ThemeTarget::Local,
2857 ThemePreference::System {
2858 family: family.to_owned(),
2859 },
2860 format!("follow subtree system family {family}"),
2861 )
2862 }
2863
2864 fn set_theme_preference(
2865 &self,
2866 target: ThemeTarget,
2867 preference: ThemePreference,
2868 message: String,
2869 ) -> Result<(), UiContextError> {
2870 self.require_mutation()?;
2871 let mut runtime = self
2872 .runtime
2873 .try_borrow_mut()
2874 .map_err(|_| UiContextError::Borrowed)?;
2875 let invalidated = match &target {
2876 ThemeTarget::App => runtime.environment_dependencies.invalidate_theme_app(),
2877 ThemeTarget::Window(window) => runtime
2878 .environment_dependencies
2879 .invalidate_theme_window(window),
2880 ThemeTarget::Local => runtime
2881 .environment_dependencies
2882 .invalidate_theme_scope(&self.component),
2883 };
2884 let app_target = matches!(&target, ThemeTarget::App);
2885 let theme = runtime
2886 .theme
2887 .as_mut()
2888 .ok_or(UiContextError::ThemeUnavailable)?;
2889 match target {
2890 ThemeTarget::App => theme.set_app(preference)?,
2891 ThemeTarget::Window(window) => theme.set_window(window, preference)?,
2892 ThemeTarget::Local => theme.set_scope(self.component.clone(), preference)?,
2893 }
2894 if app_target {
2895 runtime.mark_all_windows_dirty();
2896 } else {
2897 runtime.dirty.insert(self.component.clone());
2898 }
2899 runtime.dirty.extend(invalidated);
2900 runtime.traces.push(
2901 crate::RuntimeTraceKind::Theme,
2902 self.component.to_string(),
2903 message,
2904 None,
2905 false,
2906 );
2907 Ok(())
2908 }
2909
2910 pub fn set_reduced_motion(&self, reduced: bool) -> Result<(), UiContextError> {
2917 self.require_mutation()?;
2918 let mut runtime = self
2919 .runtime
2920 .try_borrow_mut()
2921 .map_err(|_| UiContextError::Borrowed)?;
2922 runtime.motions.request_preference(if reduced {
2923 crate::MotionPreference::Reduced
2924 } else {
2925 crate::MotionPreference::Normal
2926 });
2927 let now = runtime.clock.now();
2928 runtime.motion_values = runtime.motions.snapshot(now);
2929 runtime.dirty.insert(self.component.clone());
2930 runtime.traces.push(
2931 crate::RuntimeTraceKind::State,
2932 self.component.to_string(),
2933 format!("reduced motion {reduced}"),
2934 None,
2935 false,
2936 );
2937 Ok(())
2938 }
2939
2940 pub fn motion_handle(&self, name: &str) -> Result<crate::MotionHandle, UiContextError> {
2946 let scope = self.motion_scope();
2947 Ok(self
2948 .runtime
2949 .try_borrow()
2950 .map_err(|_| UiContextError::Borrowed)?
2951 .motions
2952 .timeline_handle_for_owner(
2953 &scope,
2954 &self.component,
2955 self.incarnation,
2956 self.generation,
2957 name,
2958 )?)
2959 }
2960
2961 #[must_use]
2962 pub fn motion_tokens(&self) -> crate::ThemeMotion {
2963 self.with_resolved_theme(|variant| variant.tokens.motion.clone())
2964 .unwrap_or_default()
2965 }
2966
2967 #[must_use]
2974 pub fn resolved_theme_variant(&self) -> Option<crate::ThemeVariantInfo> {
2975 self.with_resolved_theme(|variant| crate::ThemeVariantInfo {
2976 family: variant.family.clone(),
2977 name: variant.name.clone(),
2978 mode: variant.mode,
2979 })
2980 }
2981
2982 #[must_use]
2985 pub fn theme_variants(&self) -> Vec<crate::ThemeVariantInfo> {
2986 let Ok(runtime) = self.runtime.try_borrow() else {
2987 return Vec::new();
2988 };
2989 runtime
2990 .theme
2991 .as_ref()
2992 .map(|theme| {
2993 theme
2994 .variants()
2995 .map(|variant| crate::ThemeVariantInfo {
2996 family: variant.family.clone(),
2997 name: variant.name.clone(),
2998 mode: variant.mode,
2999 })
3000 .collect()
3001 })
3002 .unwrap_or_default()
3003 }
3004
3005 fn with_resolved_theme<R>(&self, project: impl FnOnce(&crate::ThemeVariant) -> R) -> Option<R> {
3006 let Ok(mut runtime) = self.runtime.try_borrow_mut() else {
3007 return None;
3008 };
3009 if self.phase == ExecutionPhase::Render {
3010 runtime
3011 .environment_dependencies
3012 .track_theme(self.window.as_deref(), &self.read_dependency);
3013 }
3014 let appearance = self
3015 .window
3016 .as_deref()
3017 .and_then(|window| runtime.window_appearances.get(window).copied())
3018 .unwrap_or(crate::SystemAppearance::Dark);
3019 runtime
3020 .theme
3021 .as_ref()
3022 .and_then(|theme| {
3023 theme
3024 .resolve(self.window.as_deref(), Some(&self.component), appearance)
3025 .ok()
3026 })
3027 .map(|theme| project(theme.variant()))
3028 }
3029
3030 #[must_use]
3032 pub fn resolved_theme_selection(&self) -> Option<ThemeSelection> {
3033 self.with_resolved_theme(|variant| ThemeSelection::new(&variant.family, &variant.name))
3034 }
3035
3036 pub fn motion_duration(&self, role: &str) -> Result<u64, UiContextError> {
3042 self.motion_tokens()
3043 .durations_ms
3044 .get(role)
3045 .copied()
3046 .ok_or_else(|| UiContextError::UnknownMotionToken {
3047 category: "duration",
3048 role: role.to_owned(),
3049 })
3050 }
3051
3052 pub fn motion_easing(&self, role: &str) -> Result<crate::MotionEasing, UiContextError> {
3058 self.motion_tokens()
3059 .easings
3060 .get(role)
3061 .copied()
3062 .ok_or_else(|| UiContextError::UnknownMotionToken {
3063 category: "easing",
3064 role: role.to_owned(),
3065 })
3066 }
3067
3068 pub fn motion_spring(&self, role: &str) -> Result<crate::ThemeMotionSpring, UiContextError> {
3074 self.motion_tokens()
3075 .springs
3076 .get(role)
3077 .copied()
3078 .ok_or_else(|| UiContextError::UnknownMotionToken {
3079 category: "spring",
3080 role: role.to_owned(),
3081 })
3082 }
3083
3084 pub fn motion_distance(&self, role: &str) -> Result<f64, UiContextError> {
3090 self.motion_tokens()
3091 .distances
3092 .get(role)
3093 .copied()
3094 .ok_or_else(|| UiContextError::UnknownMotionToken {
3095 category: "distance",
3096 role: role.to_owned(),
3097 })
3098 }
3099
3100 pub fn motion_stagger(&self, role: &str) -> Result<u64, UiContextError> {
3106 self.motion_tokens()
3107 .staggers_ms
3108 .get(role)
3109 .copied()
3110 .ok_or_else(|| UiContextError::UnknownMotionToken {
3111 category: "stagger",
3112 role: role.to_owned(),
3113 })
3114 }
3115
3116 #[must_use]
3117 pub fn motion_quality(&self) -> crate::MotionQuality {
3118 self.runtime
3119 .try_borrow()
3120 .map_or(crate::MotionQuality::Low, |runtime| {
3121 runtime.motions.quality()
3122 })
3123 }
3124
3125 pub fn play_motion(&self, handle: &crate::MotionHandle) -> Result<(), UiContextError> {
3131 self.require_mutation()?;
3132 let mut runtime = self
3133 .runtime
3134 .try_borrow_mut()
3135 .map_err(|_| UiContextError::Borrowed)?;
3136 runtime.motions.validate_handle_owner(
3137 handle,
3138 &self.motion_scope(),
3139 &self.component,
3140 self.incarnation,
3141 self.generation,
3142 )?;
3143 let now = runtime.clock.now();
3144 runtime.motions.play_timeline(handle, now)?;
3145 Ok(())
3146 }
3147
3148 pub fn pause_motion(&self, handle: &crate::MotionHandle) -> Result<(), UiContextError> {
3154 self.require_mutation()?;
3155 let mut runtime = self
3156 .runtime
3157 .try_borrow_mut()
3158 .map_err(|_| UiContextError::Borrowed)?;
3159 runtime.motions.validate_handle_owner(
3160 handle,
3161 &self.motion_scope(),
3162 &self.component,
3163 self.incarnation,
3164 self.generation,
3165 )?;
3166 let now = runtime.clock.now();
3167 runtime.motions.pause_timeline(handle, now)?;
3168 Ok(())
3169 }
3170
3171 pub fn seek_motion(
3177 &self,
3178 handle: &crate::MotionHandle,
3179 position_ms: u64,
3180 ) -> Result<(), UiContextError> {
3181 self.require_mutation()?;
3182 let mut runtime = self
3183 .runtime
3184 .try_borrow_mut()
3185 .map_err(|_| UiContextError::Borrowed)?;
3186 runtime.motions.validate_handle_owner(
3187 handle,
3188 &self.motion_scope(),
3189 &self.component,
3190 self.incarnation,
3191 self.generation,
3192 )?;
3193 let now = runtime.clock.now();
3194 runtime.motions.seek_timeline(handle, position_ms, now)?;
3195 runtime.motion_values = runtime.motions.snapshot(now);
3196 Ok(())
3197 }
3198
3199 pub fn restart_motion(&self, handle: &crate::MotionHandle) -> Result<(), UiContextError> {
3205 self.require_mutation()?;
3206 let mut runtime = self
3207 .runtime
3208 .try_borrow_mut()
3209 .map_err(|_| UiContextError::Borrowed)?;
3210 runtime.motions.validate_handle_owner(
3211 handle,
3212 &self.motion_scope(),
3213 &self.component,
3214 self.incarnation,
3215 self.generation,
3216 )?;
3217 let now = runtime.clock.now();
3218 runtime.motions.restart_timeline(handle, now)?;
3219 Ok(())
3220 }
3221
3222 pub fn cancel_motion(&self, handle: &crate::MotionHandle) -> Result<(), UiContextError> {
3228 self.require_mutation()?;
3229 let mut runtime = self
3230 .runtime
3231 .try_borrow_mut()
3232 .map_err(|_| UiContextError::Borrowed)?;
3233 runtime.motions.validate_handle_owner(
3234 handle,
3235 &self.motion_scope(),
3236 &self.component,
3237 self.incarnation,
3238 self.generation,
3239 )?;
3240 runtime.motions.cancel_timeline(handle)?;
3241 Ok(())
3242 }
3243
3244 fn motion_scope(&self) -> String {
3245 match (self.window.as_deref(), self.view.as_deref()) {
3246 (Some(window), Some(view)) => format!("window:{window}/view:{view}/root"),
3247 (Some(window), None) => format!("window:{window}/root"),
3248 (None, Some(view)) => format!("view:{view}/root"),
3249 (None, None) => "root".to_owned(),
3250 }
3251 }
3252
3253 pub fn load_image(&self, asset: &AssetId) -> Result<OpaqueHandle, UiContextError> {
3259 self.require_mutation()?;
3260 Ok(self
3261 .runtime
3262 .try_borrow()
3263 .map_err(|_| UiContextError::Borrowed)?
3264 .assets
3265 .load_image(asset)?
3266 .opaque()
3267 .clone())
3268 }
3269
3270 pub fn start_image_decode(
3276 &self,
3277 asset: &AssetId,
3278 success: FnPtr,
3279 error: FnPtr,
3280 ) -> Result<ImageDecodeHandle, UiContextError> {
3281 self.require_mutation()?;
3282 self.require_async_start()?;
3283 self.require_generation()?;
3284 let success = self.scoped_callback(success)?;
3285 let error = self.scoped_callback(error)?;
3286 let mut runtime = self
3287 .runtime
3288 .try_borrow_mut()
3289 .map_err(|_| UiContextError::Borrowed)?;
3290 crate::RuntimeBudgets::check(
3291 "image_decodes",
3292 runtime.assets.pending_decode_count().saturating_add(1),
3293 runtime.budgets.image_decodes,
3294 )?;
3295 let handle = runtime.assets.start_image_decode(
3296 asset,
3297 self.async_scope
3298 .clone()
3299 .unwrap_or_else(|| AsyncScope::Component(self.component.clone())),
3300 self.generation,
3301 success,
3302 error,
3303 )?;
3304 runtime.traces.push(
3305 crate::RuntimeTraceKind::Task,
3306 self.component.to_string(),
3307 format!("decode image {}", asset.as_str()),
3308 None,
3309 false,
3310 );
3311 Ok(handle)
3312 }
3313
3314 pub fn cancel_image_decode(&self, handle: ImageDecodeHandle) -> Result<bool, UiContextError> {
3320 self.require_mutation()?;
3321 let runtime = self
3322 .runtime
3323 .try_borrow()
3324 .map_err(|_| UiContextError::Borrowed)?;
3325 self.require_async_owner(runtime.assets.image_decode_scope(handle)?.as_ref())?;
3326 Ok(runtime.assets.cancel_image_decode(handle)?)
3327 }
3328
3329 pub fn cancel_task(&self, handle: TaskHandle) -> Result<bool, UiContextError> {
3335 self.require_mutation()?;
3336 let mut runtime = self
3337 .runtime
3338 .try_borrow_mut()
3339 .map_err(|_| UiContextError::Borrowed)?;
3340 self.require_async_owner(runtime.tasks.scope(handle))?;
3341 Ok(runtime.tasks.cancel(handle))
3342 }
3343
3344 pub fn cancel_subscription(&self, handle: SubscriptionHandle) -> Result<bool, UiContextError> {
3350 self.require_mutation()?;
3351 let mut runtime = self
3352 .runtime
3353 .try_borrow_mut()
3354 .map_err(|_| UiContextError::Borrowed)?;
3355 self.require_async_owner(runtime.subscriptions.scope(handle))?;
3356 Ok(runtime.subscriptions.cancel(handle))
3357 }
3358
3359 pub fn pause_timeout(&self, key: &str) -> Result<bool, UiContextError> {
3365 self.require_mutation()?;
3366 let id = crate::TimerId::new(self.component.clone(), key)?;
3367 let mut runtime = self
3368 .runtime
3369 .try_borrow_mut()
3370 .map_err(|_| UiContextError::Borrowed)?;
3371 let now = runtime.clock.now();
3372 Ok(runtime.timers.pause(&id, now))
3373 }
3374
3375 pub fn resume_timeout(&self, key: &str) -> Result<bool, UiContextError> {
3381 self.require_mutation()?;
3382 let id = crate::TimerId::new(self.component.clone(), key)?;
3383 let mut runtime = self
3384 .runtime
3385 .try_borrow_mut()
3386 .map_err(|_| UiContextError::Borrowed)?;
3387 let now = runtime.clock.now();
3388 Ok(runtime.timers.resume(&id, now))
3389 }
3390
3391 pub fn cancel_timeout(&self, key: &str) -> Result<bool, UiContextError> {
3397 self.require_mutation()?;
3398 let id = crate::TimerId::new(self.component.clone(), key)?;
3399 Ok(self
3400 .runtime
3401 .try_borrow_mut()
3402 .map_err(|_| UiContextError::Borrowed)?
3403 .timers
3404 .cancel(&id))
3405 }
3406
3407 pub fn start_task(
3413 &self,
3414 capability: &str,
3415 method: &str,
3416 input: Dynamic,
3417 success: FnPtr,
3418 error: FnPtr,
3419 ) -> Result<TaskHandle, UiContextError> {
3420 self.require_mutation()?;
3421 self.require_async_start()?;
3422 self.require_generation()?;
3423 let id = CapabilityId::parse(capability)?;
3424 let input = UiValue::from_dynamic(input)?;
3425 let success = self.scoped_callback(success)?;
3426 let error = self.scoped_callback(error)?;
3427 let mut runtime = self
3428 .runtime
3429 .try_borrow_mut()
3430 .map_err(|_| UiContextError::Borrowed)?;
3431 crate::RuntimeBudgets::check(
3432 "background_tasks",
3433 runtime.tasks.active_count().saturating_add(1),
3434 runtime.budgets.background_tasks,
3435 )?;
3436 let context = self.invocation_context(&runtime);
3437 let (work, output) = runtime
3438 .capabilities
3439 .start_task_with(&context, &id, method, input)?;
3440 let handle = runtime.tasks.spawn_cancellable(
3441 self.async_scope
3442 .clone()
3443 .unwrap_or_else(|| AsyncScope::Component(self.component.clone())),
3444 self.generation,
3445 success,
3446 error,
3447 output,
3448 move |cancellation| work.run(cancellation),
3449 )?;
3450 runtime.traces.push(
3451 crate::RuntimeTraceKind::Task,
3452 self.component.to_string(),
3453 format!("start {capability}.{method}"),
3454 None,
3455 true,
3456 );
3457 Ok(handle)
3458 }
3459
3460 pub fn start_subscription(
3466 &self,
3467 capability: &str,
3468 method: &str,
3469 input: Dynamic,
3470 success: FnPtr,
3471 error: FnPtr,
3472 options: SubscriptionOptions,
3473 ) -> Result<SubscriptionHandle, UiContextError> {
3474 self.require_mutation()?;
3475 self.require_async_start()?;
3476 self.require_generation()?;
3477 if !matches!(self.async_scope, Some(AsyncScope::Effect { .. })) {
3478 return Err(UiContextError::SubscriptionRequiresEffect);
3479 }
3480 let id = CapabilityId::parse(capability)?;
3481 let input = UiValue::from_dynamic(input)?;
3482 let success = self.scoped_callback(success)?;
3483 let error = self.scoped_callback(error)?;
3484 let mut runtime = self
3485 .runtime
3486 .try_borrow_mut()
3487 .map_err(|_| UiContextError::Borrowed)?;
3488 crate::RuntimeBudgets::check(
3489 "subscriptions",
3490 runtime.subscriptions.active_count().saturating_add(1),
3491 runtime.budgets.subscriptions,
3492 )?;
3493 let context = self.invocation_context(&runtime);
3494 let (work, output) = runtime
3495 .capabilities
3496 .start_subscription_with(&context, &id, method, input)?;
3497 let registration = SubscriptionRegistration::new(
3498 format!("{capability}.{method}"),
3499 self.async_scope
3500 .clone()
3501 .unwrap_or_else(|| AsyncScope::Component(self.component.clone())),
3502 self.generation,
3503 success,
3504 error,
3505 output,
3506 )
3507 .with_options(options);
3508 let (handle, emitter) = runtime.subscriptions.subscribe(registration);
3509 let closer = emitter.clone();
3510 if let Err(spawn_error) = std::thread::Builder::new()
3511 .name("gpui-rhai-subscription".to_owned())
3512 .spawn(move || run_subscription_work(work, emitter, &closer))
3513 {
3514 let _ = runtime
3515 .subscriptions
3516 .cancel_with_reason(handle, SubscriptionCloseReason::StartupFailed);
3517 return Err(AsyncRuntimeError::Spawn(spawn_error).into());
3518 }
3519 runtime.traces.push(
3520 crate::RuntimeTraceKind::Subscription,
3521 self.component.to_string(),
3522 format!("start {capability}.{method}"),
3523 None,
3524 true,
3525 );
3526 Ok(handle)
3527 }
3528
3529 fn require_mutation(&self) -> Result<(), UiContextError> {
3530 if self.phase.allows_mutation() {
3531 Ok(())
3532 } else {
3533 Err(UiContextError::MutationDuringRender)
3534 }
3535 }
3536
3537 fn require_async_start(&self) -> Result<(), UiContextError> {
3538 if self.phase.allows_async_start() {
3539 Ok(())
3540 } else {
3541 Err(UiContextError::AsyncDuringSuspend)
3542 }
3543 }
3544
3545 fn require_async_owner(&self, scope: Option<&AsyncScope>) -> Result<(), UiContextError> {
3548 let Some(owner) = scope.and_then(AsyncScope::component) else {
3549 return Ok(());
3550 };
3551 if owner == &self.component
3552 || self.async_scope.as_ref().and_then(AsyncScope::component) == Some(owner)
3553 {
3554 Ok(())
3555 } else {
3556 Err(UiContextError::ForeignAsyncHandle {
3557 owner: owner.clone(),
3558 })
3559 }
3560 }
3561
3562 fn require_generation(&self) -> Result<(), UiContextError> {
3563 if self.generation == ScriptGeneration::default() {
3564 Err(UiContextError::MissingGeneration)
3565 } else {
3566 Ok(())
3567 }
3568 }
3569}
3570
3571fn run_subscription_work(
3572 work: crate::SubscriptionWork,
3573 emitter: crate::SubscriptionEmitter,
3574 closer: &crate::SubscriptionEmitter,
3575) {
3576 work.run(emitter);
3577 closer.close_with_reason(SubscriptionCloseReason::WorkReturned);
3578}
3579
3580impl CustomType for UiContext {
3581 fn build(mut builder: TypeBuilder<Self>) {
3582 builder
3583 .with_name("UiContext")
3584 .with_fn(
3585 "emit",
3586 |context: &mut Self, event: ImmutableString, payload: Dynamic| {
3587 context
3588 .emit(event.as_str(), payload)
3589 .map_err(|error| Box::new(context_runtime_error(&error)))
3590 },
3591 )
3592 .with_fn(
3593 "component_style",
3594 |context: &mut Self, part: ImmutableString, base: crate::Style| {
3595 context.resolve_component_style(part.as_str(), base)
3596 },
3597 )
3598 .with_fn(
3599 "call_capability",
3600 |context: &mut Self,
3601 capability: ImmutableString,
3602 method: ImmutableString,
3603 input: Dynamic| {
3604 context
3605 .call_capability(capability.as_str(), method.as_str(), input)
3606 .map(UiValue::into_dynamic)
3607 .map_err(|error| Box::new(context_runtime_error(&error)))
3608 },
3609 );
3610 register_state_store_context_methods(&mut builder);
3611 register_native_collection_context_methods(&mut builder);
3612 register_native_document_context_methods(&mut builder);
3613 #[cfg(feature = "charts")]
3614 register_native_chart_data_context_methods(&mut builder);
3615 register_signal_context_methods(&mut builder);
3616 register_element_ref_context_methods(&mut builder);
3617 register_async_context_methods(&mut builder);
3618 register_action_context_methods(&mut builder);
3619 register_locale_context_methods(&mut builder);
3620 register_theme_context_methods(&mut builder);
3621 register_motion_context_methods(&mut builder);
3622 register_asset_context_methods(&mut builder);
3623 register_window_context_methods(&mut builder);
3624 }
3625}
3626
3627fn register_native_collection_context_methods(builder: &mut TypeBuilder<UiContext>) {
3628 builder.with_fn(
3629 "get_native_collection",
3630 |context: &mut UiContext, name: ImmutableString| {
3631 context
3632 .get_native_collection(name.as_str())
3633 .map_err(|error| Box::new(context_runtime_error(&error)))
3634 },
3635 );
3636}
3637
3638fn register_native_document_context_methods(builder: &mut TypeBuilder<UiContext>) {
3639 builder.with_fn(
3640 "get_native_text_document",
3641 |context: &mut UiContext, name: ImmutableString| {
3642 context
3643 .get_native_text_document(name.as_str())
3644 .map_err(|error| Box::new(context_runtime_error(&error)))
3645 },
3646 );
3647}
3648
3649#[cfg(feature = "charts")]
3650fn register_native_chart_data_context_methods(builder: &mut TypeBuilder<UiContext>) {
3651 builder.with_fn(
3652 "get_native_chart_data",
3653 |context: &mut UiContext, name: ImmutableString| {
3654 context
3655 .get_native_chart_data(name.as_str())
3656 .map_err(|error| Box::new(context_runtime_error(&error)))
3657 },
3658 );
3659}
3660
3661fn register_state_store_context_methods(builder: &mut TypeBuilder<UiContext>) {
3662 register_state_context_methods(builder);
3663 register_app_store_context_methods(builder);
3664 register_window_store_context_methods(builder);
3665}
3666
3667fn register_state_context_methods(builder: &mut TypeBuilder<UiContext>) {
3668 builder
3669 .with_fn(
3670 "get_state",
3671 |context: &mut UiContext, field: ImmutableString| {
3672 context
3673 .get_state(field.as_str())
3674 .map(UiValue::into_dynamic)
3675 .map_err(|error| Box::new(context_runtime_error(&error)))
3676 },
3677 )
3678 .with_fn(
3679 "get_state_path",
3680 |context: &mut UiContext, field: ImmutableString, path: Array| {
3681 let path = rhai_value_path(path)?;
3682 context
3683 .get_state_path(field.as_str(), &path)
3684 .map(UiValue::into_dynamic)
3685 .map_err(|error| Box::new(context_runtime_error(&error)))
3686 },
3687 )
3688 .with_fn(
3689 "set_state",
3690 |context: &mut UiContext, field: ImmutableString, value: Dynamic| {
3691 context
3692 .set_state(field.as_str(), value)
3693 .map_err(|error| Box::new(context_runtime_error(&error)))
3694 },
3695 )
3696 .with_fn(
3697 "set_state_path",
3698 |context: &mut UiContext, field: ImmutableString, path: Array, value: Dynamic| {
3699 let path = rhai_value_path(path)?;
3700 context
3701 .set_state_path(field.as_str(), &path, value)
3702 .map_err(|error| Box::new(context_runtime_error(&error)))
3703 },
3704 );
3705}
3706
3707fn register_app_store_context_methods(builder: &mut TypeBuilder<UiContext>) {
3708 builder
3709 .with_fn(
3710 "get_app_store",
3711 |context: &mut UiContext, store: ImmutableString, field: ImmutableString| {
3712 context
3713 .get_app_store(store.as_str(), field.as_str())
3714 .map(UiValue::into_dynamic)
3715 .map_err(|error| Box::new(context_runtime_error(&error)))
3716 },
3717 )
3718 .with_fn(
3719 "get_app_store_path",
3720 |context: &mut UiContext,
3721 store: ImmutableString,
3722 field: ImmutableString,
3723 path: Array| {
3724 let path = rhai_value_path(path)?;
3725 context
3726 .get_app_store_path(store.as_str(), field.as_str(), &path)
3727 .map(UiValue::into_dynamic)
3728 .map_err(|error| Box::new(context_runtime_error(&error)))
3729 },
3730 )
3731 .with_fn(
3732 "set_app_store",
3733 |context: &mut UiContext,
3734 store: ImmutableString,
3735 field: ImmutableString,
3736 value: Dynamic| {
3737 context
3738 .set_app_store(store.as_str(), field.as_str(), value)
3739 .map_err(|error| Box::new(context_runtime_error(&error)))
3740 },
3741 )
3742 .with_fn(
3743 "set_app_store_path",
3744 |context: &mut UiContext,
3745 store: ImmutableString,
3746 field: ImmutableString,
3747 path: Array,
3748 value: Dynamic| {
3749 let path = rhai_value_path(path)?;
3750 context
3751 .set_app_store_path(store.as_str(), field.as_str(), &path, value)
3752 .map_err(|error| Box::new(context_runtime_error(&error)))
3753 },
3754 );
3755}
3756
3757fn register_window_store_context_methods(builder: &mut TypeBuilder<UiContext>) {
3758 builder
3759 .with_fn(
3760 "get_window_store",
3761 |context: &mut UiContext, store: ImmutableString, field: ImmutableString| {
3762 context
3763 .get_window_store(store.as_str(), field.as_str())
3764 .map(UiValue::into_dynamic)
3765 .map_err(|error| Box::new(context_runtime_error(&error)))
3766 },
3767 )
3768 .with_fn(
3769 "get_window_store_path",
3770 |context: &mut UiContext,
3771 store: ImmutableString,
3772 field: ImmutableString,
3773 path: Array| {
3774 let path = rhai_value_path(path)?;
3775 context
3776 .get_window_store_path(store.as_str(), field.as_str(), &path)
3777 .map(UiValue::into_dynamic)
3778 .map_err(|error| Box::new(context_runtime_error(&error)))
3779 },
3780 )
3781 .with_fn(
3782 "set_window_store",
3783 |context: &mut UiContext,
3784 store: ImmutableString,
3785 field: ImmutableString,
3786 value: Dynamic| {
3787 context
3788 .set_window_store(store.as_str(), field.as_str(), value)
3789 .map_err(|error| Box::new(context_runtime_error(&error)))
3790 },
3791 )
3792 .with_fn(
3793 "set_window_store_path",
3794 |context: &mut UiContext,
3795 store: ImmutableString,
3796 field: ImmutableString,
3797 path: Array,
3798 value: Dynamic| {
3799 let path = rhai_value_path(path)?;
3800 context
3801 .set_window_store_path(store.as_str(), field.as_str(), &path, value)
3802 .map_err(|error| Box::new(context_runtime_error(&error)))
3803 },
3804 );
3805}
3806
3807fn rhai_value_path(values: Array) -> Result<UiValuePath, Box<EvalAltResult>> {
3808 if values.len() > 64 {
3809 return Err(Box::new(EvalAltResult::ErrorRuntime(
3810 "value path must have at most 64 segments".into(),
3811 Position::NONE,
3812 )));
3813 }
3814 let segments = values
3815 .into_iter()
3816 .enumerate()
3817 .map(|(index, value)| {
3818 let actual = value.type_name().to_owned();
3819 if value.is::<ImmutableString>() {
3820 return Ok(UiValuePathSegment::Key(
3821 value.cast::<ImmutableString>().to_string(),
3822 ));
3823 }
3824 if value.is::<INT>() {
3825 let index_value = value.cast::<INT>();
3826 return usize::try_from(index_value)
3827 .map(UiValuePathSegment::Index)
3828 .map_err(|_| {
3829 Box::new(EvalAltResult::ErrorRuntime(
3830 format!("value path segment {index} must be a non-negative integer")
3831 .into(),
3832 Position::NONE,
3833 ))
3834 });
3835 }
3836 if let Some(mut selector) = value.try_cast::<Map>() {
3837 let by = selector
3838 .remove("by")
3839 .and_then(Dynamic::try_cast::<ImmutableString>);
3840 let key = selector
3841 .remove("key")
3842 .and_then(Dynamic::try_cast::<ImmutableString>);
3843 if selector.is_empty()
3844 && let (Some(by), Some(key)) = (by, key)
3845 {
3846 return Ok(UiValuePathSegment::Item {
3847 key_field: by.to_string(),
3848 key: key.to_string(),
3849 });
3850 }
3851 return Err(Box::new(EvalAltResult::ErrorRuntime(
3852 format!(
3853 "value path segment {index} keyed selector must be exactly #{{ by: string, key: string }}"
3854 )
3855 .into(),
3856 Position::NONE,
3857 )));
3858 }
3859 Err(Box::new(EvalAltResult::ErrorRuntime(
3860 format!(
3861 "value path segment {index} must be a string, non-negative integer, or keyed selector map; got {actual}"
3862 )
3863 .into(),
3864 Position::NONE,
3865 )))
3866 })
3867 .collect::<Result<Vec<_>, _>>()?;
3868 UiValuePath::new(segments).map_err(|error| {
3869 Box::new(EvalAltResult::ErrorRuntime(
3870 error.to_string().into(),
3871 Position::NONE,
3872 ))
3873 })
3874}
3875
3876fn register_signal_context_methods(builder: &mut TypeBuilder<UiContext>) {
3877 builder
3878 .with_fn(
3879 "get_signal",
3880 |context: &mut UiContext, signal: crate::NativeSignal| {
3881 context
3882 .get_signal(&signal)
3883 .map_err(|error| Box::new(context_runtime_error(&error)))
3884 },
3885 )
3886 .with_fn(
3887 "get_signal",
3888 |context: &mut UiContext, key: ImmutableString| {
3889 context
3890 .get_signal_by_key(key.as_str())
3891 .map_err(|error| Box::new(context_runtime_error(&error)))
3892 },
3893 )
3894 .with_fn(
3895 "set_signal",
3896 |context: &mut UiContext, signal: crate::NativeSignal, value: Dynamic| {
3897 context
3898 .set_signal(&signal, value)
3899 .map_err(|error| Box::new(context_runtime_error(&error)))
3900 },
3901 )
3902 .with_fn(
3903 "set_signal",
3904 |context: &mut UiContext, key: ImmutableString, value: Dynamic| {
3905 context
3906 .set_signal_by_key(key.as_str(), value)
3907 .map_err(|error| Box::new(context_runtime_error(&error)))
3908 },
3909 );
3910}
3911
3912fn register_element_ref_context_methods(builder: &mut TypeBuilder<UiContext>) {
3913 builder
3914 .with_fn(
3915 "element_bounds",
3916 |context: &mut UiContext, reference: crate::ElementRef| {
3917 context
3918 .element_bounds(&reference)
3919 .map(UiValue::into_dynamic)
3920 .map_err(|error| Box::new(context_runtime_error(&error)))
3921 },
3922 )
3923 .with_fn(
3924 "element_bounds",
3925 |context: &mut UiContext, key: ImmutableString| {
3926 context
3927 .element_bounds_by_key(key.as_str())
3928 .map(UiValue::into_dynamic)
3929 .map_err(|error| Box::new(context_runtime_error(&error)))
3930 },
3931 )
3932 .with_fn("event_target_bounds", |context: &mut UiContext| {
3933 context
3934 .event_target_bounds()
3935 .map(UiValue::into_dynamic)
3936 .map_err(|error| Box::new(context_runtime_error(&error)))
3937 })
3938 .with_fn(
3939 "virtual_item_bounds",
3940 |context: &mut UiContext, collection: ImmutableString, index: rhai::INT| {
3941 let index = usize::try_from(index).map_err(|_| {
3942 Box::new(EvalAltResult::ErrorRuntime(
3943 "virtual item index must be non-negative".into(),
3944 Position::NONE,
3945 ))
3946 })?;
3947 context
3948 .virtual_item_bounds(collection.as_str(), index)
3949 .map(UiValue::into_dynamic)
3950 .map_err(|error| Box::new(context_runtime_error(&error)))
3951 },
3952 )
3953 .with_fn(
3954 "focus",
3955 |context: &mut UiContext, reference: crate::ElementRef| {
3956 context
3957 .focus_element(&reference)
3958 .map_err(|error| Box::new(context_runtime_error(&error)))
3959 },
3960 )
3961 .with_fn("focus", |context: &mut UiContext, key: ImmutableString| {
3962 context
3963 .focus_element_by_key(key.as_str())
3964 .map_err(|error| Box::new(context_runtime_error(&error)))
3965 });
3966}
3967
3968fn geometry_bounds_value(bounds: crate::GeometryBounds) -> UiValue {
3969 bounds.into_value()
3970}
3971
3972#[allow(clippy::too_many_lines)]
3973fn register_action_context_methods(builder: &mut TypeBuilder<UiContext>) {
3974 builder
3975 .with_fn(
3976 "dispatch_action",
3977 |context: &mut UiContext, action: ImmutableString, payload: Dynamic| {
3978 context
3979 .dispatch_action(action.as_str(), payload)
3980 .map_err(|error| Box::new(context_runtime_error(&error)))
3981 },
3982 )
3983 .with_fn(
3984 "register_action",
3985 |call: NativeCallContext<'_>,
3986 context: &mut UiContext,
3987 action: ImmutableString,
3988 callback: FnPtr| {
3989 context_at_call(context, &call)
3990 .register_action(action.as_str(), callback)
3991 .map_err(|error| Box::new(context_runtime_error(&error)))
3992 },
3993 )
3994 .with_fn(
3995 "set_action_enabled",
3996 |context: &mut UiContext, action: ImmutableString, enabled: bool| {
3997 context
3998 .set_action_enabled(action.as_str(), enabled)
3999 .map_err(|error| Box::new(context_runtime_error(&error)))
4000 },
4001 )
4002 .with_fn(
4003 "action_shortcut",
4004 |context: &mut UiContext, action: ImmutableString| {
4005 context
4006 .action_shortcut(action.as_str())
4007 .map(|shortcut| {
4008 shortcut.map_or(Dynamic::UNIT, |shortcut| {
4009 shortcut.to_ui_value().into_dynamic()
4010 })
4011 })
4012 .map_err(|error| Box::new(context_runtime_error(&error)))
4013 },
4014 )
4015 .with_fn(
4016 "action_enabled",
4017 |context: &mut UiContext, action: ImmutableString| {
4018 context
4019 .action_enabled(action.as_str())
4020 .map_err(|error| Box::new(context_runtime_error(&error)))
4021 },
4022 )
4023 .with_fn("actions", |context: &mut UiContext| {
4024 context
4025 .actions()
4026 .map(|actions| {
4027 actions
4028 .into_iter()
4029 .map(UiValue::into_dynamic)
4030 .collect::<rhai::Array>()
4031 })
4032 .map_err(|error| Box::new(context_runtime_error(&error)))
4033 })
4034 .with_fn(
4035 "scroll_to",
4036 |context: &mut UiContext, reference: crate::ElementRef, x: FLOAT, y: FLOAT| {
4037 context
4038 .scroll_element_to(&reference, x, y)
4039 .map_err(|error| Box::new(context_runtime_error(&error)))
4040 },
4041 )
4042 .with_fn(
4043 "scroll_to",
4044 |context: &mut UiContext, key: ImmutableString, x: FLOAT, y: FLOAT| {
4045 context
4046 .scroll_element_to_by_key(key.as_str(), x, y)
4047 .map_err(|error| Box::new(context_runtime_error(&error)))
4048 },
4049 )
4050 .with_fn(
4052 "scroll_to",
4053 |context: &mut UiContext,
4054 reference: crate::ElementRef,
4055 x: Dynamic,
4056 y: Dynamic|
4057 -> Result<(), Box<EvalAltResult>> {
4058 context
4059 .scroll_element_to(&reference, scroll_number(&x)?, scroll_number(&y)?)
4060 .map_err(|error| Box::new(context_runtime_error(&error)))
4061 },
4062 )
4063 .with_fn(
4064 "scroll_to",
4065 |context: &mut UiContext,
4066 key: ImmutableString,
4067 x: Dynamic,
4068 y: Dynamic|
4069 -> Result<(), Box<EvalAltResult>> {
4070 context
4071 .scroll_element_to_by_key(key.as_str(), scroll_number(&x)?, scroll_number(&y)?)
4072 .map_err(|error| Box::new(context_runtime_error(&error)))
4073 },
4074 )
4075 .with_fn(
4076 "scroll_into_view",
4077 |context: &mut UiContext, reference: crate::ElementRef| {
4078 context
4079 .scroll_element_into_view(&reference)
4080 .map_err(|error| Box::new(context_runtime_error(&error)))
4081 },
4082 )
4083 .with_fn(
4084 "scroll_into_view",
4085 |context: &mut UiContext, key: ImmutableString| {
4086 context
4087 .scroll_element_into_view_by_key(key.as_str())
4088 .map_err(|error| Box::new(context_runtime_error(&error)))
4089 },
4090 );
4091}
4092
4093fn register_async_context_methods(builder: &mut TypeBuilder<UiContext>) {
4094 builder
4095 .with_fn(
4096 "start_task",
4097 |call: NativeCallContext<'_>,
4098 context: &mut UiContext,
4099 capability: ImmutableString,
4100 method: ImmutableString,
4101 input: Dynamic,
4102 success: FnPtr,
4103 error: FnPtr| {
4104 context_at_call(context, &call)
4105 .start_task(capability.as_str(), method.as_str(), input, success, error)
4106 .map_err(|error| Box::new(context_runtime_error(&error)))
4107 },
4108 )
4109 .with_fn(
4110 "start_subscription",
4111 |call: NativeCallContext<'_>,
4112 context: &mut UiContext,
4113 capability: ImmutableString,
4114 method: ImmutableString,
4115 input: Dynamic,
4116 success: FnPtr,
4117 error: FnPtr,
4118 options: Map| {
4119 let options = subscription_options(options)?;
4120 context_at_call(context, &call)
4121 .start_subscription(
4122 capability.as_str(),
4123 method.as_str(),
4124 input,
4125 success,
4126 error,
4127 options,
4128 )
4129 .map_err(|error| Box::new(context_runtime_error(&error)))
4130 },
4131 )
4132 .with_fn(
4133 "cancel_task",
4134 |context: &mut UiContext, handle: TaskHandle| {
4135 context
4136 .cancel_task(handle)
4137 .map_err(|error| Box::new(context_runtime_error(&error)))
4138 },
4139 )
4140 .with_fn(
4141 "cancel_subscription",
4142 |context: &mut UiContext, handle: SubscriptionHandle| {
4143 context
4144 .cancel_subscription(handle)
4145 .map_err(|error| Box::new(context_runtime_error(&error)))
4146 },
4147 )
4148 .with_fn(
4149 "pause_timeout",
4150 |context: &mut UiContext, key: ImmutableString| {
4151 context
4152 .pause_timeout(key.as_str())
4153 .map_err(|error| Box::new(context_runtime_error(&error)))
4154 },
4155 )
4156 .with_fn(
4157 "resume_timeout",
4158 |context: &mut UiContext, key: ImmutableString| {
4159 context
4160 .resume_timeout(key.as_str())
4161 .map_err(|error| Box::new(context_runtime_error(&error)))
4162 },
4163 )
4164 .with_fn(
4165 "cancel_timeout",
4166 |context: &mut UiContext, key: ImmutableString| {
4167 context
4168 .cancel_timeout(key.as_str())
4169 .map_err(|error| Box::new(context_runtime_error(&error)))
4170 },
4171 );
4172}
4173
4174fn subscription_options(mut options: Map) -> Result<SubscriptionOptions, Box<EvalAltResult>> {
4175 let defaults = SubscriptionOptions::default();
4176 let delivery = options.remove("delivery").map_or(
4177 Ok(defaults.delivery()),
4178 |value| -> Result<_, Box<EvalAltResult>> {
4179 let value = value.try_cast::<ImmutableString>().ok_or_else(|| {
4180 Box::new(EvalAltResult::ErrorRuntime(
4181 "subscription delivery must be `all` or `latest`".into(),
4182 Position::NONE,
4183 ))
4184 })?;
4185 match value.as_str() {
4186 "all" => Ok(SubscriptionDeliveryPolicy::All),
4187 "latest" => Ok(SubscriptionDeliveryPolicy::Latest),
4188 _ => Err(Box::new(EvalAltResult::ErrorRuntime(
4189 "subscription delivery must be `all` or `latest`".into(),
4190 Position::NONE,
4191 ))),
4192 }
4193 },
4194 )?;
4195 let capacity = options.remove("capacity").map_or(
4196 Ok(defaults.capacity()),
4197 |value| -> Result<_, Box<EvalAltResult>> {
4198 value
4199 .try_cast::<INT>()
4200 .and_then(|value| usize::try_from(value).ok())
4201 .ok_or_else(|| {
4202 Box::new(EvalAltResult::ErrorRuntime(
4203 "subscription capacity must be a positive integer".into(),
4204 Position::NONE,
4205 ))
4206 })
4207 },
4208 )?;
4209 let throttle = options.remove("throttle_ms").map_or(
4210 Ok(defaults.throttle()),
4211 |value| -> Result<_, Box<EvalAltResult>> {
4212 value
4213 .try_cast::<INT>()
4214 .and_then(|value| u64::try_from(value).ok())
4215 .map(Duration::from_millis)
4216 .ok_or_else(|| {
4217 Box::new(EvalAltResult::ErrorRuntime(
4218 "subscription throttle_ms must be a non-negative integer".into(),
4219 Position::NONE,
4220 ))
4221 })
4222 },
4223 )?;
4224 if let Some(name) = options.keys().next() {
4225 return Err(Box::new(EvalAltResult::ErrorRuntime(
4226 format!("unknown subscription option `{name}`").into(),
4227 Position::NONE,
4228 )));
4229 }
4230 SubscriptionOptions::new(delivery, capacity, throttle).map_err(|error| {
4231 Box::new(EvalAltResult::ErrorRuntime(
4232 error.to_string().into(),
4233 Position::NONE,
4234 ))
4235 })
4236}
4237
4238fn register_locale_context_methods(builder: &mut TypeBuilder<UiContext>) {
4239 builder
4240 .with_fn("t", |context: &mut UiContext, key: ImmutableString| {
4241 context
4242 .text(key.as_str())
4243 .map_err(|error| Box::new(context_runtime_error(&error)))
4244 })
4245 .with_fn(
4246 "set_locale",
4247 |context: &mut UiContext, locale: ImmutableString| {
4248 context
4249 .set_locale(locale.as_str())
4250 .map_err(|error| Box::new(context_runtime_error(&error)))
4251 },
4252 )
4253 .with_fn("text_direction", |context: &mut UiContext| {
4254 context
4255 .text_direction()
4256 .map_err(|error| Box::new(context_runtime_error(&error)))
4257 })
4258 .with_fn("today", |context: &mut UiContext| {
4259 context
4260 .today()
4261 .map_err(|error| Box::new(context_runtime_error(&error)))
4262 })
4263 .with_fn(
4264 "format_date",
4265 |context: &mut UiContext, date: ImmutableString, style: ImmutableString| {
4266 context
4267 .format_date(date.as_str(), style.as_str())
4268 .map_err(|error| Box::new(context_runtime_error(&error)))
4269 },
4270 )
4271 .with_fn(
4272 "format_month_year",
4273 |context: &mut UiContext, date: ImmutableString| {
4274 context
4275 .format_month_year(date.as_str())
4276 .map_err(|error| Box::new(context_runtime_error(&error)))
4277 },
4278 )
4279 .with_fn("calendar", |context: &mut UiContext| {
4280 context
4281 .calendar_metadata()
4282 .map_err(|error| Box::new(context_runtime_error(&error)))
4283 })
4284 .with_fn("number", |context: &mut UiContext| {
4285 context
4286 .number_metadata()
4287 .map_err(|error| Box::new(context_runtime_error(&error)))
4288 })
4289 .with_fn("format_number", |context: &mut UiContext, value: INT| {
4290 context
4291 .format_integer(value, NumberFormatOptions::default())
4292 .map_err(|error| Box::new(context_runtime_error(&error)))
4293 })
4294 .with_fn("format_number", |context: &mut UiContext, value: FLOAT| {
4295 context
4296 .format_number(value, NumberFormatOptions::default())
4297 .map_err(|error| Box::new(context_runtime_error(&error)))
4298 })
4299 .with_fn(
4300 "format_number",
4301 |context: &mut UiContext,
4302 value: INT,
4303 options: Map|
4304 -> Result<String, Box<EvalAltResult>> {
4305 let options = number_format_options(options)?;
4306 context
4307 .format_integer(value, options)
4308 .map_err(|error| Box::new(context_runtime_error(&error)))
4309 },
4310 )
4311 .with_fn(
4312 "format_number",
4313 |context: &mut UiContext,
4314 value: FLOAT,
4315 options: Map|
4316 -> Result<String, Box<EvalAltResult>> {
4317 let options = number_format_options(options)?;
4318 context
4319 .format_number(value, options)
4320 .map_err(|error| Box::new(context_runtime_error(&error)))
4321 },
4322 );
4323}
4324
4325fn number_format_options(mut options: Map) -> Result<NumberFormatOptions, Box<EvalAltResult>> {
4326 let mut parsed = NumberFormatOptions::default();
4327 if let Some(value) = options.remove("min_fraction_digits") {
4328 let value = value.try_cast::<INT>().ok_or_else(|| {
4329 Box::new(EvalAltResult::ErrorRuntime(
4330 "min_fraction_digits must be an integer".into(),
4331 Position::NONE,
4332 ))
4333 })?;
4334 parsed.min_fraction_digits = u8::try_from(value).map_err(|_| {
4335 Box::new(EvalAltResult::ErrorRuntime(
4336 "min_fraction_digits must be between 0 and 12".into(),
4337 Position::NONE,
4338 ))
4339 })?;
4340 }
4341 if let Some(value) = options.remove("max_fraction_digits") {
4342 let value = value.try_cast::<INT>().ok_or_else(|| {
4343 Box::new(EvalAltResult::ErrorRuntime(
4344 "max_fraction_digits must be an integer".into(),
4345 Position::NONE,
4346 ))
4347 })?;
4348 parsed.max_fraction_digits = u8::try_from(value).map_err(|_| {
4349 Box::new(EvalAltResult::ErrorRuntime(
4350 "max_fraction_digits must be between 0 and 12".into(),
4351 Position::NONE,
4352 ))
4353 })?;
4354 }
4355 if let Some(value) = options.remove("grouping") {
4356 parsed.grouping = value.try_cast::<bool>().ok_or_else(|| {
4357 Box::new(EvalAltResult::ErrorRuntime(
4358 "grouping must be a bool".into(),
4359 Position::NONE,
4360 ))
4361 })?;
4362 }
4363 if let Some((unknown, _)) = options.into_iter().next() {
4364 return Err(Box::new(EvalAltResult::ErrorRuntime(
4365 format!("unknown number format option `{unknown}`").into(),
4366 Position::NONE,
4367 )));
4368 }
4369 parsed.validate().map_err(|error| {
4370 Box::new(EvalAltResult::ErrorRuntime(
4371 error.to_string().into(),
4372 Position::NONE,
4373 ))
4374 })?;
4375 Ok(parsed)
4376}
4377
4378fn theme_variant_map(variant: &crate::ThemeVariantInfo) -> Dynamic {
4379 let mut map = rhai::Map::new();
4380 map.insert("family".into(), Dynamic::from(variant.family.clone()));
4381 map.insert("name".into(), Dynamic::from(variant.name.clone()));
4382 map.insert(
4383 "mode".into(),
4384 Dynamic::from(match variant.mode {
4385 crate::ThemeMode::Light => "light",
4386 crate::ThemeMode::Dark => "dark",
4387 }),
4388 );
4389 Dynamic::from(map)
4390}
4391
4392fn register_theme_context_methods(builder: &mut TypeBuilder<UiContext>) {
4393 builder
4394 .with_fn(
4395 "theme_variant",
4396 |context: &mut UiContext| -> Result<Dynamic, Box<EvalAltResult>> {
4397 Ok(context
4398 .resolved_theme_variant()
4399 .map_or(Dynamic::UNIT, |variant| theme_variant_map(&variant)))
4400 },
4401 )
4402 .with_fn("theme_variants", |context: &mut UiContext| -> rhai::Array {
4403 context
4404 .theme_variants()
4405 .iter()
4406 .map(theme_variant_map)
4407 .collect()
4408 })
4409 .with_fn(
4410 "set_theme",
4411 |context: &mut UiContext, family: ImmutableString, variant: ImmutableString| {
4412 context
4413 .set_theme(family.as_str(), variant.as_str())
4414 .map_err(|error| Box::new(context_runtime_error(&error)))
4415 },
4416 )
4417 .with_fn(
4418 "set_window_theme",
4419 |context: &mut UiContext, family: ImmutableString, variant: ImmutableString| {
4420 context
4421 .set_window_theme(family.as_str(), variant.as_str())
4422 .map_err(|error| Box::new(context_runtime_error(&error)))
4423 },
4424 )
4425 .with_fn(
4426 "set_local_theme",
4427 |context: &mut UiContext, family: ImmutableString, variant: ImmutableString| {
4428 context
4429 .set_local_theme(family.as_str(), variant.as_str())
4430 .map_err(|error| Box::new(context_runtime_error(&error)))
4431 },
4432 )
4433 .with_fn(
4434 "set_theme_system",
4435 |context: &mut UiContext, family: ImmutableString| {
4436 context
4437 .set_theme_system(family.as_str())
4438 .map_err(|error| Box::new(context_runtime_error(&error)))
4439 },
4440 )
4441 .with_fn(
4442 "set_window_theme_system",
4443 |context: &mut UiContext, family: ImmutableString| {
4444 context
4445 .set_window_theme_system(family.as_str())
4446 .map_err(|error| Box::new(context_runtime_error(&error)))
4447 },
4448 )
4449 .with_fn(
4450 "set_local_theme_system",
4451 |context: &mut UiContext, family: ImmutableString| {
4452 context
4453 .set_local_theme_system(family.as_str())
4454 .map_err(|error| Box::new(context_runtime_error(&error)))
4455 },
4456 );
4457}
4458
4459#[allow(clippy::too_many_lines)]
4460fn register_motion_context_methods(builder: &mut TypeBuilder<UiContext>) {
4461 builder
4462 .with_fn(
4463 "set_reduced_motion",
4464 |context: &mut UiContext, reduced: bool| {
4465 context
4466 .set_reduced_motion(reduced)
4467 .map_err(|error| Box::new(context_runtime_error(&error)))
4468 },
4469 )
4470 .with_fn(
4471 "motion_handle",
4472 |context: &mut UiContext, name: ImmutableString| {
4473 context
4474 .motion_handle(name.as_str())
4475 .map_err(|error| Box::new(context_runtime_error(&error)))
4476 },
4477 )
4478 .with_fn(
4479 "motion_duration",
4480 |context: &mut UiContext, role: ImmutableString| {
4481 context
4482 .motion_duration(role.as_str())
4483 .map(|value| INT::try_from(value).unwrap_or(INT::MAX))
4484 .map_err(|error| Box::new(context_runtime_error(&error)))
4485 },
4486 )
4487 .with_fn(
4488 "motion_easing",
4489 |context: &mut UiContext, role: ImmutableString| {
4490 context
4491 .motion_easing(role.as_str())
4492 .map(|value| match value {
4493 crate::MotionEasing::Linear => "linear",
4494 crate::MotionEasing::EaseIn => "ease_in",
4495 crate::MotionEasing::EaseOut => "ease_out",
4496 crate::MotionEasing::EaseInOut => "ease_in_out",
4497 })
4498 .map(ImmutableString::from)
4499 .map_err(|error| Box::new(context_runtime_error(&error)))
4500 },
4501 )
4502 .with_fn(
4503 "motion_spring",
4504 |context: &mut UiContext, role: ImmutableString| {
4505 context
4506 .motion_spring(role.as_str())
4507 .map(|spring| {
4508 Map::from_iter([
4509 ("stiffness".into(), Dynamic::from_float(spring.stiffness)),
4510 ("damping".into(), Dynamic::from_float(spring.damping)),
4511 ("mass".into(), Dynamic::from_float(spring.mass)),
4512 ])
4513 })
4514 .map_err(|error| Box::new(context_runtime_error(&error)))
4515 },
4516 )
4517 .with_fn(
4518 "motion_distance",
4519 |context: &mut UiContext, role: ImmutableString| {
4520 context
4521 .motion_distance(role.as_str())
4522 .map_err(|error| Box::new(context_runtime_error(&error)))
4523 },
4524 )
4525 .with_fn(
4526 "motion_stagger",
4527 |context: &mut UiContext, role: ImmutableString| {
4528 context
4529 .motion_stagger(role.as_str())
4530 .map(|value| INT::try_from(value).unwrap_or(INT::MAX))
4531 .map_err(|error| Box::new(context_runtime_error(&error)))
4532 },
4533 )
4534 .with_fn("motion_quality", |context: &mut UiContext| {
4535 match context.motion_quality() {
4536 crate::MotionQuality::Low => "low",
4537 crate::MotionQuality::Medium => "medium",
4538 crate::MotionQuality::High => "high",
4539 }
4540 })
4541 .with_fn(
4542 "play_motion",
4543 |context: &mut UiContext, handle: crate::MotionHandle| {
4544 context
4545 .play_motion(&handle)
4546 .map_err(|error| Box::new(context_runtime_error(&error)))
4547 },
4548 )
4549 .with_fn(
4550 "resume_motion",
4551 |context: &mut UiContext, handle: crate::MotionHandle| {
4552 context
4553 .play_motion(&handle)
4554 .map_err(|error| Box::new(context_runtime_error(&error)))
4555 },
4556 )
4557 .with_fn(
4558 "pause_motion",
4559 |context: &mut UiContext, handle: crate::MotionHandle| {
4560 context
4561 .pause_motion(&handle)
4562 .map_err(|error| Box::new(context_runtime_error(&error)))
4563 },
4564 )
4565 .with_fn(
4566 "seek_motion",
4567 |context: &mut UiContext, handle: crate::MotionHandle, position_ms: INT| {
4568 let position_ms = u64::try_from(position_ms).map_err(|_| {
4569 Box::new(context_runtime_error(&UiContextError::Motion(
4570 crate::MotionError::InvalidTimeline(
4571 "seek position must be non-negative".to_owned(),
4572 ),
4573 )))
4574 })?;
4575 context
4576 .seek_motion(&handle, position_ms)
4577 .map_err(|error| Box::new(context_runtime_error(&error)))
4578 },
4579 )
4580 .with_fn(
4581 "restart_motion",
4582 |context: &mut UiContext, handle: crate::MotionHandle| {
4583 context
4584 .restart_motion(&handle)
4585 .map_err(|error| Box::new(context_runtime_error(&error)))
4586 },
4587 )
4588 .with_fn(
4589 "cancel_motion",
4590 |context: &mut UiContext, handle: crate::MotionHandle| {
4591 context
4592 .cancel_motion(&handle)
4593 .map_err(|error| Box::new(context_runtime_error(&error)))
4594 },
4595 );
4596}
4597
4598fn register_asset_context_methods(builder: &mut TypeBuilder<UiContext>) {
4599 builder
4600 .with_fn("load_image", |context: &mut UiContext, asset: AssetId| {
4601 context
4602 .load_image(&asset)
4603 .map_err(|error| Box::new(context_runtime_error(&error)))
4604 })
4605 .with_fn(
4606 "start_image_decode",
4607 |call: NativeCallContext<'_>,
4608 context: &mut UiContext,
4609 asset: AssetId,
4610 success: FnPtr,
4611 error: FnPtr| {
4612 context_at_call(context, &call)
4613 .start_image_decode(&asset, success, error)
4614 .map_err(|error| Box::new(context_runtime_error(&error)))
4615 },
4616 )
4617 .with_fn(
4618 "cancel_image_decode",
4619 |context: &mut UiContext, handle: ImageDecodeHandle| {
4620 context
4621 .cancel_image_decode(handle)
4622 .map_err(|error| Box::new(context_runtime_error(&error)))
4623 },
4624 );
4625}
4626
4627fn register_window_context_methods(builder: &mut TypeBuilder<UiContext>) {
4628 builder
4629 .with_fn("window_id", |context: &mut UiContext| {
4630 context
4631 .window_id()
4632 .map_err(|error| Box::new(context_runtime_error(&error)))
4633 })
4634 .with_fn("view_id", |context: &mut UiContext| {
4635 context
4636 .view_id()
4637 .map_err(|error| Box::new(context_runtime_error(&error)))
4638 })
4639 .with_fn("viewport_class", |context: &mut UiContext| {
4640 context
4641 .viewport_class()
4642 .map_err(|error| Box::new(context_runtime_error(&error)))
4643 })
4644 .with_fn(
4645 "open_window",
4646 |context: &mut UiContext,
4647 id: ImmutableString,
4648 title: ImmutableString,
4649 width: rhai::INT,
4650 height: rhai::INT,
4651 focus: bool| {
4652 context
4653 .open_window(ScriptWindowSpec {
4654 id: id.to_string(),
4655 title: title.to_string(),
4656 width: width.to_string().parse::<f64>().unwrap_or(f64::INFINITY),
4657 height: height.to_string().parse::<f64>().unwrap_or(f64::INFINITY),
4658 focus,
4659 })
4660 .map_err(|error| Box::new(context_runtime_error(&error)))
4661 },
4662 )
4663 .with_fn(
4664 "focus_window",
4665 |context: &mut UiContext, id: ImmutableString| {
4666 context
4667 .focus_window(id.as_str())
4668 .map_err(|error| Box::new(context_runtime_error(&error)))
4669 },
4670 )
4671 .with_fn(
4672 "close_window",
4673 |context: &mut UiContext, id: ImmutableString| {
4674 context
4675 .close_window(id.as_str())
4676 .map_err(|error| Box::new(context_runtime_error(&error)))
4677 },
4678 )
4679 .with_fn(
4680 "set_close_handler",
4681 |call: NativeCallContext<'_>, context: &mut UiContext, callback: FnPtr| {
4682 context_at_call(context, &call)
4683 .set_close_handler(callback)
4684 .map_err(|error| Box::new(context_runtime_error(&error)))
4685 },
4686 )
4687 .with_fn("clear_close_handler", |context: &mut UiContext| {
4688 context
4689 .clear_close_handler()
4690 .map_err(|error| Box::new(context_runtime_error(&error)))
4691 })
4692 .with_fn(
4693 "set_window_locale",
4694 |context: &mut UiContext, locale: ImmutableString| {
4695 context
4696 .set_window_locale(locale.as_str())
4697 .map_err(|error| Box::new(context_runtime_error(&error)))
4698 },
4699 )
4700 .with_fn(
4701 "set_local_locale",
4702 |context: &mut UiContext, locale: ImmutableString| {
4703 context
4704 .set_local_locale(locale.as_str())
4705 .map_err(|error| Box::new(context_runtime_error(&error)))
4706 },
4707 );
4708}
4709
4710fn context_at_call(context: &UiContext, call: &NativeCallContext<'_>) -> UiContext {
4711 context.clone().with_native_context(Some(
4712 crate::invocation::ScriptInvocationContext::capture_retained(call),
4713 ))
4714}
4715
4716pub(crate) fn register_ui_context_api(engine: &mut Engine) {
4717 engine.build_type::<UiContext>();
4718}
4719
4720fn scroll_number(value: &Dynamic) -> Result<f64, Box<EvalAltResult>> {
4721 crate::value::script_number(value).ok_or_else(|| {
4722 Box::new(EvalAltResult::ErrorRuntime(
4723 format!("scroll offset must be a number, got {}", value.type_name()).into(),
4724 Position::NONE,
4725 ))
4726 })
4727}
4728
4729fn context_runtime_error(error: &UiContextError) -> EvalAltResult {
4730 EvalAltResult::ErrorRuntime(error.to_string().into(), Position::NONE)
4731}
4732
4733#[derive(Debug, Error)]
4734pub enum UiContextError {
4735 #[error("UI runtime state is already mutably borrowed by another callback")]
4736 Borrowed,
4737 #[error("state mutation and effects are forbidden during view rendering")]
4738 MutationDuringRender,
4739 #[error("new async work is forbidden while a view is suspending")]
4740 AsyncDuringSuspend,
4741 #[error("subscriptions must be started by a declarative component effect")]
4742 SubscriptionRequiresEffect,
4743 #[error("event target geometry is available only during event callbacks")]
4744 EventTargetOutsideEvent,
4745 #[error("async work requires a bound script generation")]
4746 MissingGeneration,
4747 #[error("this async handle belongs to component `{owner}`; only that component may cancel it")]
4748 ForeignAsyncHandle { owner: ComponentInstancePath },
4749 #[error("no locale manager is configured for this application")]
4750 LocaleUnavailable,
4751 #[error("no theme manager is configured for this application")]
4752 ThemeUnavailable,
4753 #[cfg(feature = "charts")]
4754 #[error("native chart data name `{0}` must be a safe identifier")]
4755 InvalidChartDataName(String),
4756 #[cfg(feature = "charts")]
4757 #[error("native chart data `{0}` is not registered")]
4758 UnknownChartData(String),
4759 #[error("unknown theme motion {category} token `{role}`")]
4760 UnknownMotionToken {
4761 category: &'static str,
4762 role: String,
4763 },
4764 #[error("component `{component}` has no state field `{field}`")]
4765 UnknownState {
4766 component: ComponentInstancePath,
4767 field: String,
4768 },
4769 #[error("event `{0}` is not declared by this component")]
4770 UnknownEvent(String),
4771 #[error("event payload is invalid: {0}")]
4772 InvalidEvent(crate::SchemaValidationError),
4773 #[error("this UI context is not associated with a window")]
4774 MissingWindow,
4775 #[error("this UI context is not associated with a mounted script view")]
4776 MissingView,
4777 #[error(transparent)]
4778 Window(#[from] WindowCommandError),
4779 #[error(transparent)]
4780 Responsive(#[from] ResponsiveError),
4781 #[error(transparent)]
4782 State(#[from] StateError),
4783 #[error(transparent)]
4784 Store(#[from] StoreError),
4785 #[error(transparent)]
4786 NativeCollection(#[from] crate::NativeCollectionError),
4787 #[error(transparent)]
4788 Document(#[from] crate::DocumentError),
4789 #[error(transparent)]
4790 Action(#[from] ActionError),
4791 #[error(transparent)]
4792 Capability(#[from] CapabilityError),
4793 #[error(transparent)]
4794 Async(#[from] AsyncRuntimeError),
4795 #[error(transparent)]
4796 Locale(#[from] LocaleError),
4797 #[error(transparent)]
4798 Theme(#[from] ThemeError),
4799 #[error(transparent)]
4800 Asset(#[from] AssetError),
4801 #[error(transparent)]
4802 Value(#[from] UiValueError),
4803 #[error(transparent)]
4804 ValuePath(#[from] UiValuePathError),
4805 #[error(transparent)]
4806 Signal(#[from] crate::SignalError),
4807 #[error(transparent)]
4808 ElementRef(#[from] crate::ElementRefError),
4809 #[error(transparent)]
4810 Geometry(#[from] crate::GeometryError),
4811 #[error("scroll offset must be finite and non-negative, got ({x}, {y})")]
4812 InvalidScrollOffset { x: f64, y: f64 },
4813 #[error(transparent)]
4814 Callback(#[from] crate::ScriptCallbackDefinitionError),
4815 #[error(transparent)]
4816 Timer(#[from] crate::TimerError),
4817 #[error(transparent)]
4818 Motion(#[from] crate::MotionError),
4819 #[error(transparent)]
4820 Budget(#[from] crate::RuntimeBudgetError),
4821}
4822
4823#[cfg(test)]
4824mod tests {
4825 use super::*;
4826 use crate::{ComponentStateSchema, RuntimeEngine, StateField, SubscriptionWork, ValueSchema};
4827
4828 fn mounted_context(phase: ExecutionPhase) -> UiContext {
4829 let path = ComponentInstancePath::root("Counter", "counter");
4830 let schema = ComponentStateSchema::new(BTreeMap::from([
4831 (
4832 "count".to_owned(),
4833 StateField::new(ValueSchema::integer(), UiValue::Integer(0)),
4834 ),
4835 (
4836 "profile".to_owned(),
4837 StateField::new(
4838 ValueSchema::UiValue,
4839 UiValue::Map(BTreeMap::from([(
4840 "name".to_owned(),
4841 UiValue::String("Ada".to_owned()),
4842 )])),
4843 ),
4844 ),
4845 ]))
4846 .unwrap();
4847 let mut state = UiRuntimeState::new();
4848 let mut render = state.component_state.begin_render();
4849 render.mount(path.clone(), &schema).unwrap();
4850 state.component_state.commit_render(render);
4851 UiContext::new(
4852 Rc::new(RefCell::new(state)),
4853 path,
4854 Some("main".to_owned()),
4855 phase,
4856 BTreeMap::from([(
4857 "change".to_owned(),
4858 EventSchema {
4859 doc: None,
4860 payload: ValueSchema::integer(),
4861 },
4862 )]),
4863 )
4864 }
4865
4866 #[test]
4867 fn theme_variant_reads_the_resolved_selection() {
4868 let context = mounted_context(ExecutionPhase::Event);
4869 assert!(context.resolved_theme_variant().is_none());
4871 assert_eq!(context.resolved_theme_selection(), None);
4872
4873 let engine = RuntimeEngine::new();
4874 let dark = crate::load_theme_source(
4875 engine.engine(),
4876 "default_dark.rhai",
4877 include_str!("../../../registry/themes/default_dark.rhai"),
4878 )
4879 .unwrap();
4880 let mut light = dark.clone();
4881 light.name = "Light".to_owned();
4882 light.mode = crate::ThemeMode::Light;
4883 let manager = crate::ThemeManager::from_variants(
4884 [dark, light],
4885 crate::ThemeSelection::new("Default".to_owned(), "Dark".to_owned()),
4886 )
4887 .unwrap();
4888 context.runtime.borrow_mut().theme = Some(manager);
4889
4890 let variant = context.resolved_theme_variant().expect("resolves");
4891 assert_eq!(variant.mode, crate::ThemeMode::Dark);
4892 assert_eq!(
4893 context.resolved_theme_selection(),
4894 Some(ThemeSelection::new("Default", "Dark"))
4895 );
4896 }
4897
4898 #[test]
4899 fn first_native_appearance_replaces_dark_fallback_and_invalidates_render_readers_once() {
4900 let context = mounted_context(ExecutionPhase::Render);
4901 let engine = RuntimeEngine::new();
4902 let dark = crate::load_theme_source(
4903 engine.engine(),
4904 "dark",
4905 include_str!("../../../registry/themes/default_dark.rhai"),
4906 )
4907 .unwrap();
4908 let light = crate::load_theme_source(
4909 engine.engine(),
4910 "light",
4911 include_str!("../../../registry/themes/default_light.rhai"),
4912 )
4913 .unwrap();
4914 let mut manager = crate::ThemeManager::from_variants(
4915 [dark, light],
4916 ThemeSelection::new("Default", "Dark"),
4917 )
4918 .unwrap();
4919 manager
4920 .set_app(ThemePreference::System {
4921 family: "Default".into(),
4922 })
4923 .unwrap();
4924 context.runtime.borrow_mut().theme = Some(manager);
4925 assert_eq!(
4926 context.resolved_theme_variant().unwrap().mode,
4927 crate::ThemeMode::Dark
4928 );
4929 {
4930 let mut runtime = context.runtime.borrow_mut();
4931 runtime.dirty.clear();
4932 assert!(runtime.update_window_appearance("main", crate::SystemAppearance::Light));
4933 assert_eq!(
4934 runtime.dirty,
4935 BTreeSet::from([context.component_path().clone()])
4936 );
4937 runtime.dirty.clear();
4938 assert!(!runtime.update_window_appearance("main", crate::SystemAppearance::Light));
4939 assert!(runtime.dirty.is_empty());
4940 }
4941 assert_eq!(
4942 context.resolved_theme_variant().unwrap().mode,
4943 crate::ThemeMode::Light
4944 );
4945 context
4946 .runtime
4947 .borrow_mut()
4948 .update_window_appearance("main", crate::SystemAppearance::Dark);
4949 assert_eq!(
4950 context.resolved_theme_variant().unwrap().mode,
4951 crate::ThemeMode::Dark
4952 );
4953 }
4954
4955 #[test]
4956 fn render_signal_reads_disable_component_reuse() {
4957 let context = mounted_context(ExecutionPhase::Render);
4958 let signal = crate::NativeSignal::new(
4959 crate::SignalId::new(
4960 context.component_path().clone(),
4961 "hot",
4962 crate::SignalKind::Float,
4963 )
4964 .unwrap(),
4965 );
4966 assert!(context.get_signal(&signal).is_err());
4967 assert!(!context.component_render_is_reusable());
4968 }
4969
4970 #[test]
4971 fn image_decodes_are_new_async_work_and_never_start_while_suspending() {
4972 for phase in [ExecutionPhase::Suspend, ExecutionPhase::Dispose] {
4973 assert!(
4974 matches!(
4975 mounted_context(phase).start_image_decode(
4976 &AssetId::parse("app/avatar").unwrap(),
4977 FnPtr::new("loaded").unwrap(),
4978 FnPtr::new("failed").unwrap(),
4979 ),
4980 Err(UiContextError::AsyncDuringSuspend)
4981 ),
4982 "{phase:?}"
4983 );
4984 }
4985 }
4986
4987 #[test]
4988 fn async_handles_cancel_only_from_the_component_that_owns_them() {
4989 let mut engine = RuntimeEngine::new();
4990 let compiled = engine
4991 .compile(
4992 "fn view() { text(\"owner\") } fn done(ctx, value) {} fn failed(ctx, error) {}",
4993 )
4994 .unwrap();
4995 let generation = compiled.generation();
4996 let callback = |name| engine.callback(&compiled, name).unwrap();
4997 let owner = ComponentInstancePath::root("View", "main").child("Owner", "owner");
4998 let other = ComponentInstancePath::root("View", "main").child("Other", "other");
4999 let mut state = UiRuntimeState::new();
5000 let spawn = |state: &mut UiRuntimeState, scope| {
5001 state
5002 .tasks
5003 .spawn(
5004 scope,
5005 generation,
5006 callback("done"),
5007 callback("failed"),
5008 ValueSchema::Null,
5009 || Ok(UiValue::Null),
5010 )
5011 .unwrap()
5012 };
5013 let task = spawn(&mut state, AsyncScope::Component(owner.clone()));
5014 let app_task = spawn(&mut state, AsyncScope::App);
5015 let (subscription, _emitter) =
5016 state.subscriptions.subscribe(SubscriptionRegistration::new(
5017 "watch",
5018 AsyncScope::Effect {
5019 component: owner.clone(),
5020 key: "watch".into(),
5021 activation: 1,
5022 },
5023 generation,
5024 callback("done"),
5025 callback("failed"),
5026 ValueSchema::integer(),
5027 ));
5028 let runtime = Rc::new(RefCell::new(state));
5029 let context = |component: &ComponentInstancePath| {
5030 UiContext::new(
5031 Rc::clone(&runtime),
5032 component.clone(),
5033 Some("main".to_owned()),
5034 ExecutionPhase::Event,
5035 BTreeMap::new(),
5036 )
5037 };
5038
5039 let foreign = context(&other);
5040 assert!(matches!(
5041 foreign.cancel_task(task),
5042 Err(UiContextError::ForeignAsyncHandle { owner: found }) if found == owner
5043 ));
5044 assert!(matches!(
5045 foreign.cancel_subscription(subscription),
5046 Err(UiContextError::ForeignAsyncHandle { owner: found }) if found == owner
5047 ));
5048 assert!(
5049 foreign.cancel_task(app_task).unwrap(),
5050 "app-scoped work has no component owner"
5051 );
5052 let own = context(&owner);
5053 assert!(own.cancel_task(task).unwrap());
5054 assert!(own.cancel_subscription(subscription).unwrap());
5055 assert!(
5056 !foreign.cancel_task(task).unwrap(),
5057 "a finished handle is no longer anyone's"
5058 );
5059 }
5060
5061 #[test]
5062 fn only_render_reads_subscribe_to_stores_and_locale() {
5063 let engine = Engine::new();
5064 let en = crate::load_locale_source(
5065 &engine,
5066 "en.rhai",
5067 include_str!("../../../registry/locales/en.rhai"),
5068 )
5069 .unwrap();
5070 let zh = crate::load_locale_source(
5071 &engine,
5072 "zh_cn.rhai",
5073 include_str!("../../../registry/locales/zh_cn.rhai"),
5074 )
5075 .unwrap();
5076 let mut state = UiRuntimeState::new();
5077 state.locale = Some(LocaleManager::new([en, zh], "en", "en").unwrap());
5078 state.windows.register_open("main").unwrap();
5079 for store in [StoreId::app("data"), StoreId::window("main", "data")] {
5080 state
5081 .stores
5082 .declare(
5083 store,
5084 ComponentStateSchema::new(BTreeMap::from([(
5085 "count".to_owned(),
5086 StateField::new(ValueSchema::integer(), UiValue::Integer(0)),
5087 )]))
5088 .unwrap(),
5089 )
5090 .unwrap();
5091 }
5092 let runtime = Rc::new(RefCell::new(state));
5093 let reader = ComponentInstancePath::root("View", "main").child("Reader", "reader");
5094 let writer = ComponentInstancePath::root("View", "main").child("Writer", "writer");
5095 let context = |component: &ComponentInstancePath, phase| {
5096 UiContext::new(
5097 Rc::clone(&runtime),
5098 component.clone(),
5099 Some("main".to_owned()),
5100 phase,
5101 BTreeMap::new(),
5102 )
5103 };
5104 let read_everything = |context: &UiContext| {
5105 context.get_app_store("data", "count").unwrap();
5106 context.get_window_store("data", "count").unwrap();
5107 context.text("common.loading").unwrap();
5108 };
5109 let write_everything = |value: i64, locale: &str| {
5110 let writer = context(&writer, ExecutionPhase::Event);
5111 writer
5112 .set_app_store("data", "count", Dynamic::from(value))
5113 .unwrap();
5114 writer
5115 .set_window_store("data", "count", Dynamic::from(value))
5116 .unwrap();
5117 writer.set_locale(locale).unwrap();
5118 runtime.borrow_mut().drain_batch().dirty
5119 };
5120
5121 read_everything(&context(&reader, ExecutionPhase::Event));
5122 assert!(
5123 !write_everything(1, "zh-CN").contains(&reader),
5124 "a callback read is no dependency"
5125 );
5126
5127 read_everything(&context(&reader, ExecutionPhase::Render));
5128 assert!(write_everything(2, "en").contains(&reader));
5129 }
5130
5131 #[test]
5132 fn render_context_rejects_mutation() {
5133 let context = mounted_context(ExecutionPhase::Render);
5134 assert!(matches!(
5135 context.set_state("count", Dynamic::from(1_i64)),
5136 Err(UiContextError::MutationDuringRender)
5137 ));
5138 }
5139
5140 #[test]
5141 fn event_target_bounds_are_untracked_and_event_only() {
5142 let context = mounted_context(ExecutionPhase::Event).with_event_target(Some(
5143 crate::GeometryBounds::new(13.0, 24.0, 90.0, 40.0).unwrap(),
5144 ));
5145 assert_eq!(
5146 context.event_target_bounds().unwrap(),
5147 crate::GeometryBounds::new(13.0, 24.0, 90.0, 40.0)
5148 .unwrap()
5149 .into_value()
5150 );
5151 assert!(context.runtime().borrow().geometry.take_dirty().is_empty());
5152
5153 assert!(matches!(
5154 mounted_context(ExecutionPhase::Render).event_target_bounds(),
5155 Err(UiContextError::EventTargetOutsideEvent)
5156 ));
5157 assert_eq!(
5158 mounted_context(ExecutionPhase::Event)
5159 .event_target_bounds()
5160 .unwrap(),
5161 UiValue::Null
5162 );
5163 }
5164
5165 #[test]
5166 fn viewport_class_is_window_scoped_and_script_readable() {
5167 let context = mounted_context(ExecutionPhase::Render);
5168 context
5169 .runtime()
5170 .borrow_mut()
5171 .responsive
5172 .update_window("main", 480.0)
5173 .unwrap();
5174 assert_eq!(context.viewport_class().unwrap(), "compact");
5175 }
5176
5177 #[test]
5178 fn locale_and_viewport_reads_register_exact_component_dependencies() {
5179 let engine = Engine::new();
5180 let en = crate::load_locale_source(
5181 &engine,
5182 "en.rhai",
5183 include_str!("../../../registry/locales/en.rhai"),
5184 )
5185 .unwrap();
5186 let zh = crate::load_locale_source(
5187 &engine,
5188 "zh_cn.rhai",
5189 include_str!("../../../registry/locales/zh_cn.rhai"),
5190 )
5191 .unwrap();
5192 let mut state = UiRuntimeState::new();
5193 state.locale = Some(LocaleManager::new([en, zh], "en", "en").unwrap());
5194 state.responsive.update_window("main", 480.0).unwrap();
5195 state.windows.register_open("main").unwrap();
5196 let runtime = Rc::new(RefCell::new(state));
5197 let reader = ComponentInstancePath::root("View", "main").child("Reader", "reader");
5198 let unrelated = ComponentInstancePath::root("View", "main").child("Static", "static");
5199 let render_context = UiContext::new(
5200 Rc::clone(&runtime),
5201 reader.clone(),
5202 Some("main".to_owned()),
5203 ExecutionPhase::Render,
5204 BTreeMap::new(),
5205 );
5206 assert_ne!(render_context.text("common.loading").unwrap(), "");
5207 assert_eq!(render_context.viewport_class().unwrap(), "compact");
5208 let event = UiContext::new(
5209 Rc::clone(&runtime),
5210 unrelated,
5211 Some("main".to_owned()),
5212 ExecutionPhase::Event,
5213 BTreeMap::new(),
5214 );
5215 event.set_locale("zh-CN").unwrap();
5216
5217 assert_eq!(
5218 runtime.borrow_mut().drain_batch().dirty,
5219 BTreeSet::from([reader.clone()])
5220 );
5221 assert_eq!(
5222 runtime
5223 .borrow()
5224 .environment_dependencies
5225 .invalidate_viewport("main"),
5226 BTreeSet::from([reader])
5227 );
5228 assert!(runtime.borrow_mut().take_window_repaint("main"));
5229 }
5230
5231 #[test]
5232 fn event_mutations_are_batched() {
5233 let context = mounted_context(ExecutionPhase::Event);
5234 context.set_state("count", Dynamic::from(1_i64)).unwrap();
5235 context.set_state("count", Dynamic::from(2_i64)).unwrap();
5236 context.emit("change", Dynamic::from(2_i64)).unwrap();
5237
5238 let mut runtime = context.runtime().borrow_mut();
5239 let batch = runtime.drain_batch();
5240 assert_eq!(batch.dirty.len(), 1);
5241 assert_eq!(batch.events.len(), 1);
5242 assert_eq!(
5243 runtime
5244 .component_state
5245 .get(&ComponentInstancePath::root("Counter", "counter"), "count",),
5246 Some(&UiValue::Integer(2))
5247 );
5248 }
5249
5250 #[test]
5251 fn semantic_event_and_action_traces_never_retain_payload_values() {
5252 let secret = "AUDIT_ONLY_SYNTHETIC_TOKEN";
5253 let path = ComponentInstancePath::root("SecretInput", "audit");
5254 let schema = ComponentStateSchema::new(BTreeMap::from([(
5255 "token".to_owned(),
5256 StateField::new(ValueSchema::string(), UiValue::String(secret.to_owned()))
5257 .sensitive(true),
5258 )]))
5259 .unwrap();
5260 let mut state = UiRuntimeState::new();
5261 let mut render = state.component_state.begin_render();
5262 render.mount(path.clone(), &schema).unwrap();
5263 state.component_state.commit_render(render);
5264
5265 let mut engine = RuntimeEngine::new();
5266 let compiled = engine
5267 .compile("fn view() { text(\"trace\") } fn receive(ctx, payload) {}")
5268 .unwrap();
5269 state.actions.register_or_replace(
5270 crate::ActionId::parse("audit.receive").unwrap(),
5271 engine.callback(&compiled, "receive").unwrap(),
5272 );
5273 let runtime = Rc::new(RefCell::new(state));
5274 let context = UiContext::new(
5275 Rc::clone(&runtime),
5276 path,
5277 Some("main".to_owned()),
5278 ExecutionPhase::Event,
5279 BTreeMap::from([(
5280 "change".to_owned(),
5281 EventSchema {
5282 doc: None,
5283 payload: ValueSchema::string(),
5284 },
5285 )]),
5286 );
5287
5288 let event_value = context.get_state("token").unwrap().into_dynamic();
5289 context.emit("change", event_value).unwrap();
5290 let action_value = context.get_state("token").unwrap().into_dynamic();
5291 context
5292 .dispatch_action("audit.receive", action_value)
5293 .unwrap();
5294
5295 let traces = runtime.borrow().traces.snapshot();
5296 let semantic = traces
5297 .iter()
5298 .filter(|trace| {
5299 matches!(
5300 trace.kind,
5301 crate::RuntimeTraceKind::Event | crate::RuntimeTraceKind::Action
5302 )
5303 })
5304 .collect::<Vec<_>>();
5305 assert_eq!(semantic.len(), 2);
5306 assert!(
5307 semantic
5308 .iter()
5309 .all(|trace| trace.sensitive && trace.payload.is_none())
5310 );
5311 assert!(!format!("{traces:?}").contains(secret));
5312 }
5313
5314 #[test]
5315 fn scripts_receive_restricted_context_without_gpui_types() {
5316 let context = mounted_context(ExecutionPhase::Event);
5317 let mut runtime = RuntimeEngine::new();
5318 let compiled = runtime
5319 .compile(
5320 r#"
5321 fn view() { text("context") }
5322 fn increment(ctx) {
5323 ctx.set_state("count", ctx.get_state("count") + 1);
5324 }
5325 "#,
5326 )
5327 .unwrap();
5328 runtime.render(&compiled).unwrap();
5329 let callback = runtime.callback(&compiled, "increment").unwrap();
5330 let _ = runtime
5331 .invoke_callback(&compiled, &callback, (context.clone(),))
5332 .unwrap();
5333 assert_eq!(context.get_state("count").unwrap(), UiValue::Integer(1));
5334 }
5335
5336 #[test]
5337 fn structural_store_paths_invalidate_executable_owner_not_synthetic_scope() {
5338 for window_store in [false, true] {
5339 let runtime = Rc::new(RefCell::new(UiRuntimeState::new()));
5340 let owner = ComponentInstancePath::root("Panel", "paths");
5341 let store = if window_store {
5342 StoreId::window("main", "data")
5343 } else {
5344 StoreId::app("data")
5345 };
5346 runtime
5347 .borrow_mut()
5348 .stores
5349 .declare(
5350 store,
5351 ComponentStateSchema::new(BTreeMap::from([(
5352 "model".into(),
5353 StateField::new(
5354 ValueSchema::UiValue,
5355 UiValue::Map(BTreeMap::from([
5356 ("label".into(), UiValue::String("before".into())),
5357 ("sibling".into(), UiValue::String("untouched".into())),
5358 ])),
5359 ),
5360 )]))
5361 .unwrap(),
5362 )
5363 .unwrap();
5364 let render = UiContext::new(
5365 Rc::clone(&runtime),
5366 owner.clone(),
5367 Some("main".into()),
5368 ExecutionPhase::Render,
5369 BTreeMap::new(),
5370 );
5371 let structural = render
5372 .for_structural_scope(owner.child("VirtualCollection", "rows"), BTreeMap::new());
5373 let label = UiValuePath::new(vec![UiValuePathSegment::Key("label".into())]).unwrap();
5374 let sibling =
5375 UiValuePath::new(vec![UiValuePathSegment::Key("sibling".into())]).unwrap();
5376 let value = if window_store {
5377 structural.get_window_store_path("data", "model", &label)
5378 } else {
5379 structural.get_app_store_path("data", "model", &label)
5380 }
5381 .unwrap();
5382 assert_eq!(value, UiValue::String("before".into()));
5383 let mut event = render.clone();
5384 event.phase = ExecutionPhase::Event;
5385 let write = |path: &UiValuePath| {
5386 if window_store {
5387 event.set_window_store_path("data", "model", path, Dynamic::from("after"))
5388 } else {
5389 event.set_app_store_path("data", "model", path, Dynamic::from("after"))
5390 }
5391 .unwrap();
5392 };
5393 write(&sibling);
5394 assert!(runtime.borrow().dirty.is_empty());
5395 write(&label);
5396 assert_eq!(runtime.borrow().dirty, BTreeSet::from([owner]));
5397 }
5398 }
5399
5400 #[test]
5401 fn scripts_use_bounded_nested_and_keyed_store_paths() {
5402 let context = mounted_context(ExecutionPhase::Event);
5403 let row = |id: &str, label: &str| {
5404 UiValue::Map(BTreeMap::from([
5405 ("id".to_owned(), UiValue::String(id.to_owned())),
5406 ("label".to_owned(), UiValue::String(label.to_owned())),
5407 ]))
5408 };
5409 context
5410 .runtime()
5411 .borrow_mut()
5412 .stores
5413 .declare(
5414 StoreId::app("model"),
5415 ComponentStateSchema::new(BTreeMap::from([(
5416 "data".to_owned(),
5417 StateField::new(
5418 ValueSchema::UiValue,
5419 UiValue::Map(BTreeMap::from([(
5420 "rows".to_owned(),
5421 UiValue::Array(vec![row("alpha", "Alpha"), row("beta", "Beta")]),
5422 )])),
5423 ),
5424 )]))
5425 .unwrap(),
5426 )
5427 .unwrap();
5428 let mut runtime = RuntimeEngine::new();
5429 let compiled = runtime
5430 .compile(
5431 r#"
5432 fn view() { text("paths") }
5433 fn update(ctx) {
5434 let name = ctx.get_state_path("profile", ["name"]);
5435 ctx.set_state_path("profile", ["name"], name + " Lovelace");
5436 let label = ctx.get_app_store_path(
5437 "model",
5438 "data",
5439 ["rows", #{ by: "id", key: "beta" }, "label"]
5440 );
5441 ctx.set_app_store_path(
5442 "model",
5443 "data",
5444 ["rows", #{ by: "id", key: "beta" }, "label"],
5445 label + "!"
5446 );
5447 }
5448 "#,
5449 )
5450 .unwrap();
5451 runtime.render(&compiled).unwrap();
5452 let callback = runtime.callback(&compiled, "update").unwrap();
5453 let _: Dynamic = runtime
5454 .invoke_callback(&compiled, &callback, (context.clone(),))
5455 .unwrap();
5456
5457 let profile = context.get_state("profile").unwrap();
5458 assert_eq!(
5459 profile
5460 .get_path(
5461 &UiValuePath::new(vec![UiValuePathSegment::Key("name".to_owned())]).unwrap()
5462 )
5463 .unwrap(),
5464 &UiValue::String("Ada Lovelace".to_owned())
5465 );
5466 let row_path = UiValuePath::new(vec![
5467 UiValuePathSegment::Key("rows".to_owned()),
5468 UiValuePathSegment::Item {
5469 key_field: "id".to_owned(),
5470 key: "beta".to_owned(),
5471 },
5472 UiValuePathSegment::Key("label".to_owned()),
5473 ])
5474 .unwrap();
5475 assert_eq!(
5476 context
5477 .runtime()
5478 .borrow_mut()
5479 .stores
5480 .read_path_tracked(
5481 &ComponentInstancePath::root("Test", "reader"),
5482 &StoreId::app("model"),
5483 "data",
5484 &row_path,
5485 )
5486 .unwrap(),
5487 UiValue::String("Beta!".to_owned())
5488 );
5489 assert!(rhai_value_path(vec![Dynamic::from(-1_i64)]).is_err());
5490 }
5491
5492 #[test]
5493 fn script_theme_switch_preserves_component_state_and_ast_generation() {
5494 let context = mounted_context(ExecutionPhase::Event);
5495 context.set_state("count", Dynamic::from(7_i64)).unwrap();
5496 let mut runtime = RuntimeEngine::new();
5497 let dark = crate::load_theme_source(
5498 runtime.engine(),
5499 "default_dark.rhai",
5500 include_str!("../../../registry/themes/default_dark.rhai"),
5501 )
5502 .unwrap();
5503 let mocha = crate::load_theme_source(
5504 runtime.engine(),
5505 "catppuccin_mocha.rhai",
5506 include_str!("../../../registry/themes/catppuccin_mocha.rhai"),
5507 )
5508 .unwrap();
5509 context.runtime().borrow_mut().theme = Some(
5510 ThemeManager::from_variants([dark, mocha], ThemeSelection::new("Default", "Dark"))
5511 .unwrap(),
5512 );
5513 let compiled = runtime
5514 .compile(
5515 r#"
5516 fn view() { text("theme") }
5517 fn switch_theme(ctx) { ctx.set_theme("Catppuccin", "Mocha"); }
5518 "#,
5519 )
5520 .unwrap();
5521 runtime.render(&compiled).unwrap();
5522 let generation = compiled.generation();
5523 let callback = runtime.callback(&compiled, "switch_theme").unwrap();
5524 let _ = runtime
5525 .invoke_callback(&compiled, &callback, (context.clone(),))
5526 .unwrap();
5527
5528 assert_eq!(context.get_state("count").unwrap(), UiValue::Integer(7));
5529 assert_eq!(compiled.generation(), generation);
5530 let state = context.runtime().borrow();
5531 let theme = state
5532 .theme
5533 .as_ref()
5534 .unwrap()
5535 .resolve(Some("main"), None, crate::SystemAppearance::Dark)
5536 .unwrap();
5537 assert_eq!(theme.variant().family, "Catppuccin");
5538 assert_eq!(theme.variant().name, "Mocha");
5539 drop(state);
5540 context.set_theme_system("Default").unwrap();
5541 let state = context.runtime().borrow();
5542 assert!(matches!(
5543 state.theme.as_ref().unwrap().app_preference(),
5544 ThemePreference::System { family } if family == "Default"
5545 ));
5546 }
5547
5548 #[test]
5549 fn scroll_offsets_take_integers_and_floats() {
5550 let mut engine = Engine::new();
5551 register_ui_context_api(&mut engine);
5552 let mut scope = rhai::Scope::new();
5553 scope.push("ctx", mounted_context(ExecutionPhase::Event));
5554 for script in [
5555 r#"ctx.scroll_to("missing", 0, 0)"#,
5556 r#"ctx.scroll_to("missing", 0, 12.5)"#,
5557 ] {
5558 let error = engine
5559 .eval_with_scope::<()>(&mut scope, script)
5560 .unwrap_err();
5561 assert!(
5562 !matches!(*error, EvalAltResult::ErrorFunctionNotFound(..)),
5563 "{script}: {error}"
5564 );
5565 }
5566 let error = engine
5567 .eval_with_scope::<()>(&mut scope, r#"ctx.scroll_to("missing", "0", 0)"#)
5568 .unwrap_err();
5569 assert!(
5570 error.to_string().contains("scroll offset must be a number"),
5571 "{error}"
5572 );
5573 }
5574
5575 #[test]
5576 fn script_reads_fixed_today_calendar_and_locale_formatters() {
5577 let mut engine = Engine::new();
5578 register_ui_context_api(&mut engine);
5579 let bundle = crate::load_locale_source(
5580 &engine,
5581 "en.rhai",
5582 include_str!("../../../registry/locales/en.rhai"),
5583 )
5584 .unwrap();
5585 let mut state = UiRuntimeState::new();
5586 state.locale = Some(LocaleManager::new([bundle], "en", "en").unwrap());
5587 state.calendar_clock =
5588 crate::CalendarClock::fixed(crate::GregorianDate::parse_iso("2026-08-29").unwrap());
5589 let context = UiContext::new(
5590 Rc::new(RefCell::new(state)),
5591 ComponentInstancePath::root("App", "root"),
5592 Some("main".to_owned()),
5593 ExecutionPhase::Render,
5594 BTreeMap::new(),
5595 );
5596 let mut scope = rhai::Scope::new();
5597 scope.push("ctx", context);
5598 let values = engine
5599 .eval_with_scope::<rhai::Array>(
5600 &mut scope,
5601 r#"[
5602 ctx.today(),
5603 ctx.format_date("2024-02-29", "long"),
5604 ctx.format_number(12345, #{ min_fraction_digits: 0, max_fraction_digits: 0 }),
5605 ctx.calendar().first_weekday,
5606 ]"#,
5607 )
5608 .unwrap();
5609 assert_eq!(values[0].clone_cast::<String>(), "2026-08-29");
5610 assert_eq!(
5611 values[1].clone_cast::<String>(),
5612 "Thursday, February 29, 2024"
5613 );
5614 assert_eq!(values[2].clone_cast::<String>(), "12,345");
5615 assert_eq!(values[3].clone_cast::<String>(), "sunday");
5616 }
5617
5618 #[test]
5619 fn returning_subscription_work_closes_external_emitters_with_reason() {
5620 let mut engine = RuntimeEngine::new();
5621 let compiled = engine
5622 .compile(
5623 r#"
5624 fn view() { text("subscription") }
5625 fn success(ctx, value) { value }
5626 fn failure(ctx, error) { error }
5627 "#,
5628 )
5629 .unwrap();
5630 let generation = compiled.generation();
5631 let success = engine.callback(&compiled, "success").unwrap();
5632 let failure = engine.callback(&compiled, "failure").unwrap();
5633 let mut state = UiRuntimeState::new();
5634 let registration = SubscriptionRegistration::new(
5635 "app.stream.watch",
5636 AsyncScope::App,
5637 generation,
5638 success,
5639 failure,
5640 ValueSchema::integer(),
5641 );
5642 let (_, emitter) = state.subscriptions.subscribe(registration);
5643 let external = emitter.clone();
5644 run_subscription_work(SubscriptionWork::new(|_| {}), emitter.clone(), &emitter);
5645
5646 assert!(matches!(
5647 external.emit(UiValue::Integer(1)),
5648 Err(AsyncRuntimeError::Closed {
5649 reason: SubscriptionCloseReason::WorkReturned
5650 })
5651 ));
5652 let _ = state.subscriptions.drain(generation);
5653 assert_eq!(
5654 state.subscriptions.take_closures()[0].reason,
5655 SubscriptionCloseReason::WorkReturned
5656 );
5657 let registration = SubscriptionRegistration::new(
5658 "app.stream.trace",
5659 AsyncScope::App,
5660 generation,
5661 engine.callback(&compiled, "success").unwrap(),
5662 engine.callback(&compiled, "failure").unwrap(),
5663 ValueSchema::integer(),
5664 );
5665 let (_, emitter) = state.subscriptions.subscribe(registration);
5666 emitter.close_with_reason(SubscriptionCloseReason::WorkReturned);
5667 let _ = state.subscriptions.drain(generation);
5668 state.trace_subscription_closures();
5669 let traces = state.traces.snapshot();
5670 assert!(traces.iter().any(|trace| {
5671 trace.kind == crate::RuntimeTraceKind::Subscription
5672 && trace.message == "close app.stream.trace: the subscription work returned"
5673 }));
5674 }
5675
5676 #[test]
5677 fn receiver_subscription_forwards_external_values_until_sender_drop() {
5678 let mut engine = RuntimeEngine::new();
5679 let compiled = engine
5680 .compile(
5681 r#"
5682 fn view() { text("subscription") }
5683 fn success(ctx, value) { value }
5684 fn failure(ctx, error) { error }
5685 "#,
5686 )
5687 .unwrap();
5688 let generation = compiled.generation();
5689 let registration = SubscriptionRegistration::new(
5690 "app.stream.receiver",
5691 AsyncScope::App,
5692 generation,
5693 engine.callback(&compiled, "success").unwrap(),
5694 engine.callback(&compiled, "failure").unwrap(),
5695 ValueSchema::integer(),
5696 );
5697 let mut subscriptions = SubscriptionRegistry::new();
5698 let (_, emitter) = subscriptions.subscribe(registration);
5699 let external = emitter.clone();
5700 let closer = emitter.clone();
5701 let (sender, receiver) = std::sync::mpsc::channel();
5702 let worker = std::thread::spawn(move || {
5703 run_subscription_work(SubscriptionWork::from_receiver(receiver), emitter, &closer);
5704 });
5705 sender.send(UiValue::Integer(42)).unwrap();
5706 drop(sender);
5707 worker.join().unwrap();
5708
5709 let deliveries = subscriptions.drain(generation);
5710 assert_eq!(deliveries[0].payload, UiValue::Integer(42));
5711 assert!(matches!(
5712 external.emit(UiValue::Integer(43)),
5713 Err(AsyncRuntimeError::Closed {
5714 reason: SubscriptionCloseReason::WorkReturned
5715 })
5716 ));
5717 }
5718
5719 #[test]
5720 fn receiver_subscription_waits_for_lossless_capacity_without_ending_the_stream() {
5721 let mut engine = RuntimeEngine::new();
5722 let compiled = engine
5723 .compile(
5724 "fn view() { text(\"stream\") } fn success(ctx, value) {} fn failure(ctx, value) {}",
5725 )
5726 .unwrap();
5727 engine.render(&compiled).unwrap();
5728 let generation = compiled.generation();
5729 let registration = SubscriptionRegistration::new(
5730 "stream",
5731 AsyncScope::App,
5732 generation,
5733 engine.callback(&compiled, "success").unwrap(),
5734 engine.callback(&compiled, "failure").unwrap(),
5735 ValueSchema::integer(),
5736 )
5737 .with_capacity(1)
5738 .unwrap();
5739 let mut subscriptions = SubscriptionRegistry::new();
5740 let (_, emitter) = subscriptions.subscribe(registration);
5741 let closer = emitter.clone();
5742 let (sender, receiver) = std::sync::mpsc::channel();
5743 let worker = std::thread::spawn(move || {
5744 run_subscription_work(SubscriptionWork::from_receiver(receiver), emitter, &closer);
5745 });
5746 sender.send(UiValue::Integer(1)).unwrap();
5747 sender.send(UiValue::Integer(2)).unwrap();
5748 let deadline = std::time::Instant::now() + Duration::from_secs(1);
5749 let first = loop {
5750 let values = subscriptions.drain(generation);
5751 if !values.is_empty() {
5752 break values;
5753 }
5754 assert!(std::time::Instant::now() < deadline);
5755 std::thread::yield_now();
5756 };
5757 assert_eq!(first[0].payload, UiValue::Integer(1));
5758 let second = loop {
5759 let values = subscriptions.drain(generation);
5760 if !values.is_empty() {
5761 break values;
5762 }
5763 assert!(std::time::Instant::now() < deadline);
5764 std::thread::yield_now();
5765 };
5766 assert_eq!(second[0].payload, UiValue::Integer(2));
5767 assert_eq!(subscriptions.active_count(), 1);
5768 drop(sender);
5769 worker.join().unwrap();
5770 }
5771
5772 #[test]
5773 fn runtime_transaction_rollback_restores_cancelled_subscription() {
5774 let mut engine = RuntimeEngine::new();
5775 let compiled = engine
5776 .compile(
5777 "fn view() { text(\"test\") } fn success(ctx, value) {} fn failure(ctx, value) {}",
5778 )
5779 .unwrap();
5780 engine.render(&compiled).unwrap();
5781 let success = engine.callback(&compiled, "success").unwrap();
5782 let error = engine.callback(&compiled, "failure").unwrap();
5783 let generation = compiled.generation();
5784 let mut state = UiRuntimeState::new();
5785 let (handle, emitter) = state.subscriptions.subscribe(SubscriptionRegistration::new(
5786 "rollback",
5787 AsyncScope::App,
5788 generation,
5789 success,
5790 error,
5791 ValueSchema::integer(),
5792 ));
5793 let checkpoint = state.begin_transaction().unwrap();
5794 assert!(state.subscriptions.cancel(handle));
5795 assert_eq!(emitter.close_reason(), None);
5796 state.rollback_transaction(checkpoint).unwrap();
5797 assert_eq!(state.subscriptions.active_count(), 1);
5798 emitter.emit(UiValue::Integer(7)).unwrap();
5799 assert_eq!(state.subscriptions.drain(generation).len(), 1);
5800 }
5801
5802 #[test]
5803 fn transaction_tokens_cannot_cross_runtime_or_commit_unbalanced() {
5804 let mut first = UiRuntimeState::new();
5805 let mut second = UiRuntimeState::new();
5806 let checkpoint = first.begin_transaction().unwrap();
5807 assert!(matches!(
5808 second.rollback_transaction(checkpoint.clone()),
5809 Err(UiTransactionError::ForeignSnapshot)
5810 ));
5811 assert!(matches!(
5812 second.commit_transaction(),
5813 Err(UiTransactionError::Unbalanced { .. })
5814 ));
5815 first.rollback_transaction(checkpoint).unwrap();
5816 }
5817
5818 #[test]
5819 fn presentation_geometry_and_capture_are_isolated_per_view() {
5820 let mut first = crate::RetainedUiTree::new();
5821 first.reconcile(crate::UiNode::text("first")).unwrap();
5822 let mut second = crate::RetainedUiTree::new();
5823 second.reconcile(crate::UiNode::text("second")).unwrap();
5824 let first_node = first.root_id().unwrap();
5825 let second_node = second.root_id().unwrap();
5826 assert_eq!(
5827 first_node, second_node,
5828 "tree-local IDs intentionally overlap"
5829 );
5830
5831 let mut state = UiRuntimeState::new();
5832 state.ensure_presentation("first");
5833 state.ensure_presentation("second");
5834 let first_geometry = state.geometry_for(Some("first"));
5835 let second_geometry = state.geometry_for(Some("second"));
5836 let bounds = |width| crate::ElementGeometry {
5837 layout: crate::GeometryBounds::new(0.0, 0.0, width, 20.0).unwrap(),
5838 visual: crate::GeometryBounds::new(0.0, 0.0, width, 20.0).unwrap(),
5839 clip: None,
5840 };
5841 first_geometry.update(first_node, bounds(100.0));
5842 second_geometry.update(second_node, bounds(300.0));
5843 second_geometry.begin_frame();
5844 assert!(
5845 (first_geometry.get(first_node).unwrap().visual.width - 100.0).abs() < f64::EPSILON
5846 );
5847 assert!(first_geometry.is_presented(first_node));
5848
5849 let first_capture = state.pointer_capture_for(Some("first"));
5850 let second_capture = state.pointer_capture_for(Some("second"));
5851 first_capture.capture(0, first_node);
5852 second_capture.capture(0, second_node);
5853 assert_eq!(first_capture.captured(0), Some(first_node));
5854 assert_eq!(second_capture.captured(0), Some(second_node));
5855 }
5856
5857 #[test]
5858 fn window_release_uses_segment_boundaries_and_clears_suspend_tombstones() {
5859 let now = std::time::Instant::now();
5860 let transition = || {
5861 let mut spec =
5862 crate::MotionTransition::new(crate::MotionProperty::Opacity, 0.0, 1.0, 1_000);
5863 spec.easing = crate::MotionEasing::Linear;
5864 crate::MotionSource::Transition(spec)
5865 };
5866 let mut state = UiRuntimeState::new();
5867 let ghost_path = "window:w2/view:v2/root/ghost:1";
5868 state
5869 .motions
5870 .start_exit(
5871 ComponentInstancePath::root("UiNode", ghost_path),
5872 transition(),
5873 now,
5874 )
5875 .unwrap();
5876 state.motion_ghosts.push(crate::motion::MotionGhost {
5877 id: "1".to_owned(),
5878 node: crate::UiNode::text("ghost"),
5879 bounds: crate::GeometryBounds::new(0.0, 0.0, 10.0, 10.0).unwrap(),
5880 path: ghost_path.to_owned(),
5881 domain: "window:w2/view:v2/root".to_owned(),
5882 });
5883 state
5884 .release_window("w", &ComponentInstancePath::root("View", "v"))
5885 .unwrap();
5886 assert_eq!(state.motion_ghosts.len(), 1);
5887 assert_eq!(state.motions.resource_usage().active, 1);
5888
5889 let mut reopened = UiRuntimeState::new();
5890 let scope = "window:w/view:v/root";
5891 reopened
5892 .motions
5893 .start(
5894 ComponentInstancePath::root("UiNode", format!("{scope}/node:1")),
5895 transition(),
5896 now,
5897 )
5898 .unwrap();
5899 reopened
5900 .motions
5901 .suspend_node_scope(scope, now + Duration::from_millis(200));
5902 reopened
5903 .release_window("w", &ComponentInstancePath::root("View", "v"))
5904 .unwrap();
5905 let key = reopened
5906 .motions
5907 .start(
5908 ComponentInstancePath::root("UiNode", format!("{scope}/node:2")),
5909 transition(),
5910 now + Duration::from_millis(200),
5911 )
5912 .unwrap();
5913 let frame = reopened.motions.tick(now + Duration::from_millis(400));
5914 assert!(frame.needs_frame);
5915 assert!((frame.values[&key] - 0.2).abs() < 0.01);
5916 }
5917
5918 #[test]
5919 fn timeline_handles_cannot_cross_view_contexts() {
5920 let runtime = Rc::new(RefCell::new(UiRuntimeState::new()));
5921 let now = std::time::Instant::now();
5922 let mut transition =
5923 crate::MotionTransition::new(crate::MotionProperty::Opacity, 0.0, 1.0, 1_000);
5924 transition.easing = crate::MotionEasing::Linear;
5925 let handle = runtime
5926 .borrow_mut()
5927 .motions
5928 .start_timeline(
5929 ComponentInstancePath::root("UiNode", "window:a/view:a/root"),
5930 crate::MotionTimeline::new(
5931 "foreign",
5932 crate::MotionTimelineStep::Track(crate::MotionTrack {
5933 target: ".".to_owned(),
5934 source: crate::MotionSource::Transition(transition),
5935 }),
5936 ),
5937 now,
5938 )
5939 .unwrap();
5940 let foreign = UiContext::new(
5941 Rc::clone(&runtime),
5942 ComponentInstancePath::root("View", "b"),
5943 Some("b".to_owned()),
5944 ExecutionPhase::Event,
5945 BTreeMap::new(),
5946 )
5947 .with_view_id("b");
5948
5949 assert!(matches!(
5950 foreign.cancel_motion(&handle),
5951 Err(UiContextError::Motion(
5952 crate::MotionError::ForeignTimelineHandle(_)
5953 ))
5954 ));
5955 assert_eq!(
5956 runtime.borrow().motions.timeline_state(&handle),
5957 Some(crate::MotionPlaybackState::Playing)
5958 );
5959 }
5960
5961 #[test]
5962 fn focus_by_ref_key_queues_a_window_scoped_retained_command() {
5963 let context = mounted_context(ExecutionPhase::Event);
5964 let mut tree = crate::RetainedUiTree::new();
5965 tree.reconcile(crate::UiNode::text("field")).unwrap();
5966 let reference = crate::ElementRef::new(
5967 crate::ElementRefId::new(context.component_path().clone(), "field").unwrap(),
5968 );
5969 context.runtime().borrow_mut().element_refs.reconcile(
5970 context.component_path(),
5971 BTreeMap::from([(reference.id().clone(), tree.root_id().unwrap())]),
5972 );
5973 let node = tree.root_id().unwrap();
5974 context
5975 .runtime()
5976 .borrow()
5977 .geometry_for(Some("main"))
5978 .update(
5979 node,
5980 crate::ElementGeometry {
5981 layout: crate::GeometryBounds::new(1.0, 2.0, 120.0, 24.0).unwrap(),
5982 visual: crate::GeometryBounds::new(3.0, 4.0, 120.0, 24.0).unwrap(),
5983 clip: None,
5984 },
5985 );
5986 assert!(matches!(
5987 context.element_bounds_by_key("field").unwrap(),
5988 UiValue::Map(bounds)
5989 if matches!(bounds.get("visual"), Some(UiValue::Map(visual))
5990 if visual.get("width") == Some(&UiValue::Float(120.0)))
5991 ));
5992 context.focus_element_by_key("field").unwrap();
5993 context
5994 .scroll_element_to_by_key("field", 12.0, 24.0)
5995 .unwrap();
5996 context.scroll_element_into_view_by_key("field").unwrap();
5997 let commands = context
5998 .runtime()
5999 .borrow_mut()
6000 .take_window_element_commands("main");
6001 assert!(matches!(
6002 commands.as_slice(),
6003 [crate::element_ref::ElementCommand::Focus { node, .. },
6004 crate::element_ref::ElementCommand::ScrollTo { node: scroll_node, x, y, .. },
6005 crate::element_ref::ElementCommand::ScrollIntoView { node: reveal_node, .. }]
6006 if *node == tree.root_id().unwrap()
6007 && scroll_node == node
6008 && reveal_node == node
6009 && (*x - 12.0).abs() < f64::EPSILON
6010 && (*y - 24.0).abs() < f64::EPSILON
6011 ));
6012 }
6013
6014 #[test]
6015 fn host_motion_preference_changes_without_replacing_runtime_state() {
6016 let mut runtime = UiRuntimeState::new();
6017 runtime.motions = crate::MotionRuntime::new(crate::MotionPreference::Normal);
6018 runtime.responsive.update_window("main", 480.0).unwrap();
6019
6020 assert!(!runtime.set_motion_preference_from_host(crate::MotionPreference::Normal));
6021 assert!(runtime.set_motion_preference_from_host(crate::MotionPreference::Reduced));
6022 assert_eq!(
6023 runtime.motions.preference(),
6024 crate::MotionPreference::Reduced
6025 );
6026 assert_eq!(
6027 runtime.responsive.class("main"),
6028 crate::ViewportClass::Compact
6029 );
6030 assert!(runtime.set_motion_preference_from_host(crate::MotionPreference::None));
6031 assert_eq!(runtime.motions.preference(), crate::MotionPreference::None);
6032 }
6033
6034 #[test]
6035 fn host_can_register_a_new_native_collection_after_mount() {
6036 let mut runtime = UiRuntimeState::new();
6037 let root = ComponentInstancePath::root("View", "live");
6038 assert!(
6039 runtime
6040 .native_collections
6041 .read_tracked(&root, "live_rows")
6042 .is_err()
6043 );
6044 let collection = crate::NativeCollection::new(
6045 "id",
6046 [BTreeMap::from([(
6047 "id".to_owned(),
6048 UiValue::String("row-1".to_owned()),
6049 )])],
6050 )
6051 .unwrap();
6052 runtime
6053 .register_native_collection_from_host(&root, "live_rows", collection)
6054 .unwrap();
6055 assert!(runtime.has_window_dirty(&root));
6056 assert!(matches!(
6057 runtime.register_native_collection_from_host(
6058 &root,
6059 "live_rows",
6060 crate::NativeCollection::new("id", std::iter::empty::<BTreeMap<String, UiValue>>())
6061 .unwrap(),
6062 ),
6063 Err(crate::NativeCollectionError::DuplicateCollection(_))
6064 ));
6065 }
6066}