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