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