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