Skip to main content

gpui_rhai/
context.rs

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, AnimationKey, AnimationRuntime,
15    AssetError, AssetId, AssetRegistry, AsyncRuntimeError, AsyncScope, CalendarClock,
16    CapabilityError, CapabilityId, CapabilityRegistry, ComponentInstancePath, DateStyle,
17    EventSchema, ImageDecodeHandle, LocaleError, LocaleManager, 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
26#[derive(Clone, Copy, Debug, Eq, PartialEq)]
27pub enum ExecutionPhase {
28    Init,
29    Render,
30    Event,
31    Suspend,
32    Resume,
33    Dispose,
34}
35
36/// Identity of one continuous component mount. Logical paths may be reused
37/// after unmount, but an incarnation never is.
38#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
39pub struct ComponentIncarnation(u64);
40
41impl ComponentIncarnation {
42    pub(crate) const fn unscoped() -> Self {
43        Self(0)
44    }
45
46    #[must_use]
47    pub const fn get(self) -> u64 {
48        self.0
49    }
50}
51
52#[derive(Clone, Copy, Debug, Eq, PartialEq)]
53struct RuntimeStateId(u64);
54
55impl Default for RuntimeStateId {
56    fn default() -> Self {
57        static NEXT_ID: AtomicU64 = AtomicU64::new(1);
58        Self(NEXT_ID.fetch_add(1, Ordering::Relaxed))
59    }
60}
61
62enum ThemeTarget {
63    App,
64    Window(String),
65    Local,
66}
67
68impl ExecutionPhase {
69    const fn allows_mutation(self) -> bool {
70        matches!(
71            self,
72            Self::Init | Self::Event | Self::Suspend | Self::Resume | Self::Dispose
73        )
74    }
75
76    const fn allows_async_start(self) -> bool {
77        matches!(self, Self::Init | Self::Event | Self::Resume)
78    }
79}
80
81#[derive(Debug, Default)]
82pub struct UiRuntimeState {
83    runtime_id: RuntimeStateId,
84    transaction_depth: usize,
85    pub component_state: StateStore,
86    pub stores: StoreRegistry,
87    pub native_collections: crate::NativeCollectionRegistry,
88    pub native_documents: crate::NativeTextDocumentRegistry,
89    pub actions: ActionRegistry,
90    pub capabilities: CapabilityRegistry,
91    pub tasks: TaskRegistry,
92    pub subscriptions: SubscriptionRegistry,
93    pub timers: crate::TimerRegistry,
94    pub locale: Option<LocaleManager>,
95    pub calendar_clock: CalendarClock,
96    pub clock: crate::RuntimeClock,
97    pub theme: Option<ThemeManager>,
98    component_styles: crate::ComponentStyleSheet,
99    pub assets: AssetRegistry,
100    pub animations: AnimationRuntime,
101    pub effects: crate::EffectRegistry,
102    pub signals: crate::SignalRegistry,
103    pub element_refs: crate::ElementRefRegistry,
104    pub(crate) geometry: crate::GeometryRegistry,
105    pub(crate) pointer_capture: crate::PointerCaptureRegistry,
106    presentations: BTreeMap<String, ViewPresentationState>,
107    pub budgets: crate::RuntimeBudgets,
108    pub virtual_requests: crate::VirtualRequestRegistry,
109    pub windows: WindowCommandRegistry,
110    pub responsive: ResponsiveRuntime,
111    pub(crate) environment_dependencies:
112        crate::environment_dependency::EnvironmentDependencyRegistry,
113    pub animation_values: BTreeMap<AnimationKey, f64>,
114    pub traces: crate::TraceBuffer,
115    component_event_handlers: BTreeMap<(ComponentInstancePath, String), ScriptCallback>,
116    component_incarnations: BTreeMap<ComponentInstancePath, ComponentIncarnation>,
117    next_component_incarnation: u64,
118    dirty: BTreeSet<ComponentInstancePath>,
119    pending_events: Vec<PendingEvent>,
120    pending_actions: Vec<ActionInvocation>,
121    pending_async: Vec<crate::AsyncDelivery>,
122    pending_element_commands: Vec<crate::element_ref::ElementCommand>,
123    repaint_windows: BTreeSet<String>,
124    component_style_generation: u64,
125}
126
127impl UiRuntimeState {
128    const SUSPENDED_DELIVERY_CAPACITY: usize = 256;
129    #[must_use]
130    pub fn new() -> Self {
131        Self::default()
132    }
133
134    pub(crate) fn ensure_component_incarnation(
135        &mut self,
136        component: &ComponentInstancePath,
137    ) -> ComponentIncarnation {
138        if let Some(incarnation) = self.component_incarnations.get(component) {
139            return *incarnation;
140        }
141        let next = self.next_component_incarnation.max(1);
142        let incarnation = ComponentIncarnation(next);
143        self.next_component_incarnation = next.saturating_add(1);
144        self.component_incarnations
145            .insert(component.clone(), incarnation);
146        incarnation
147    }
148
149    pub(crate) fn component_incarnation(
150        &self,
151        component: &ComponentInstancePath,
152    ) -> Option<ComponentIncarnation> {
153        self.component_incarnations.get(component).copied()
154    }
155
156    pub(crate) fn ensure_presentation(&mut self, view: &str) {
157        self.presentations.entry(view.to_owned()).or_default();
158    }
159
160    /// Return the geometry domain for one mounted view, creating it if needed.
161    pub fn presentation_geometry(&mut self, view: &str) -> crate::GeometryRegistry {
162        self.ensure_presentation(view);
163        self.geometry_for(Some(view))
164    }
165
166    /// Return the pointer-capture domain for one mounted view, creating it if needed.
167    pub fn presentation_pointer_capture(&mut self, view: &str) -> crate::PointerCaptureRegistry {
168        self.ensure_presentation(view);
169        self.pointer_capture_for(Some(view))
170    }
171
172    pub(crate) fn geometry_for(&self, view: Option<&str>) -> crate::GeometryRegistry {
173        view.and_then(|view| self.presentations.get(view))
174            .map_or_else(|| self.geometry.clone(), |state| state.geometry.clone())
175    }
176
177    pub(crate) fn pointer_capture_for(&self, view: Option<&str>) -> crate::PointerCaptureRegistry {
178        view.and_then(|view| self.presentations.get(view))
179            .map_or_else(
180                || self.pointer_capture.clone(),
181                |state| state.pointer_capture.clone(),
182            )
183    }
184
185    pub(crate) fn remove_presentation(&mut self, view: &str) {
186        self.presentations.remove(view);
187    }
188
189    /// Set one mounted component field from trusted host code and schedule its
190    /// retained subtree for reevaluation.
191    ///
192    /// # Errors
193    ///
194    /// Returns the same schema and identity errors as [`StateStore::set`].
195    pub fn set_component_state_from_host(
196        &mut self,
197        component: &ComponentInstancePath,
198        field: &str,
199        value: UiValue,
200    ) -> Result<bool, crate::StateError> {
201        let changed = self.component_state.set(component, field, value)?;
202        if changed {
203            self.dirty.insert(component.clone());
204        }
205        Ok(changed)
206    }
207
208    /// Replace one Rust-owned collection and invalidate only subscribed components.
209    ///
210    /// # Errors
211    ///
212    /// Returns an error when the collection name is not registered.
213    pub fn replace_native_collection_from_host(
214        &mut self,
215        name: &str,
216        collection: crate::NativeCollection,
217    ) -> Result<bool, crate::NativeCollectionError> {
218        let invalidated = self.native_collections.replace(name, collection)?;
219        let changed = !invalidated.is_empty();
220        self.dirty.extend(invalidated);
221        Ok(changed)
222    }
223
224    /// Replace one Host-owned text revision and invalidate exact readers.
225    ///
226    /// # Errors
227    ///
228    /// Returns when the document name is not registered.
229    pub fn replace_native_text_document_from_host(
230        &mut self,
231        name: &str,
232        document: crate::NativeTextDocument,
233    ) -> Result<bool, crate::DocumentError> {
234        let invalidated = self.native_documents.replace(name, document)?;
235        let changed = !invalidated.is_empty();
236        self.dirty.extend(invalidated);
237        Ok(changed)
238    }
239
240    /// Replace the active editable theme variant and invalidate mounted
241    /// component subtrees without recompiling Rhai.
242    ///
243    /// # Errors
244    ///
245    /// Returns when no theme manager is installed or the variant is invalid.
246    pub fn replace_theme_variant_from_host(
247        &mut self,
248        variant: crate::ThemeVariant,
249    ) -> Result<(), crate::ThemeError> {
250        self.theme
251            .as_mut()
252            .ok_or(crate::ThemeError::NoVariants)?
253            .replace_variant(variant)?;
254        self.dirty.extend(self.component_state.paths());
255        Ok(())
256    }
257
258    /// Atomically replace application-wide formal-component style rules.
259    ///
260    /// Equal replacements are ignored. A changed sheet invalidates every
261    /// mounted component because rules are keyed by formal component identity.
262    pub fn replace_component_styles_from_host(
263        &mut self,
264        styles: crate::ComponentStyleSheet,
265    ) -> bool {
266        if self.component_styles == styles {
267            return false;
268        }
269        self.component_styles = styles;
270        self.component_style_generation = self.component_style_generation.saturating_add(1);
271        self.dirty.extend(self.component_state.paths());
272        true
273    }
274
275    pub(crate) const fn component_style_generation(&self) -> u64 {
276        self.component_style_generation
277    }
278
279    pub(crate) const fn component_styles(&self) -> &crate::ComponentStyleSheet {
280        &self.component_styles
281    }
282
283    pub(crate) fn trace_subscription_closures(&mut self) {
284        for closure in self.subscriptions.take_closures() {
285            let scope = match closure.scope {
286                AsyncScope::App => "/App".to_owned(),
287                AsyncScope::Window(window) => format!("window:{window}"),
288                AsyncScope::Component(component) => component.to_string(),
289                AsyncScope::Effect {
290                    component,
291                    key,
292                    activation,
293                } => format!("{component}/effect[{key}]#{activation}"),
294            };
295            self.traces.push(
296                crate::RuntimeTraceKind::Subscription,
297                scope,
298                format!("close {}: {}", closure.label, closure.reason),
299                None,
300                false,
301            );
302        }
303    }
304
305    /// Release every window-owned runtime resource while retaining app scope.
306    ///
307    /// # Errors
308    ///
309    /// Returns an asset-registry borrow error if image work is being drained.
310    pub fn release_window(
311        &mut self,
312        window: &str,
313        root: &ComponentInstancePath,
314    ) -> Result<(), AssetError> {
315        self.tasks
316            .cancel_scope(&AsyncScope::Window(window.to_owned()));
317        self.tasks.cancel_component_scope(root);
318        self.timers.cancel_component_scope(root);
319        self.subscriptions
320            .cancel_scope(&AsyncScope::Window(window.to_owned()));
321        self.subscriptions.cancel_component_scope(root);
322        self.assets.cancel_window_scope(window, root)?;
323        self.stores.remove_window(window);
324        self.native_collections.remove_reader_scope(root);
325        self.native_documents.remove_reader_scope(root);
326        self.component_state.remove_scope(root);
327        self.component_event_handlers
328            .retain(|(path, _), _| !path.is_within(root));
329        self.component_incarnations
330            .retain(|path, _| !path.is_within(root));
331        self.actions.remove_component_scope(root);
332        self.dirty.retain(|path| !path.is_within(root));
333        self.pending_events
334            .retain(|event| !event.target.is_within(root));
335        self.pending_actions.retain(|action| {
336            !action
337                .callback
338                .component()
339                .is_some_and(|path| path.is_within(root))
340        });
341        self.pending_async.retain(|delivery| match &delivery.scope {
342            AsyncScope::App => true,
343            AsyncScope::Window(id) => id != window,
344            AsyncScope::Component(_) | AsyncScope::Effect { .. } => {
345                !delivery.scope.is_within_component(root)
346            }
347        });
348        self.pending_element_commands
349            .retain(|command| command.window() != window);
350        self.environment_dependencies.remove_window(window);
351        self.environment_dependencies.remove_scope(root);
352        self.repaint_windows.remove(window);
353        if let Some(theme) = self.theme.as_mut() {
354            theme.remove_window(window);
355            theme.remove_scope(root);
356        }
357        if let Some(locale) = self.locale.as_mut() {
358            locale.remove_window(window);
359            locale.remove_scope(root);
360        }
361        self.animations
362            .cancel_node_scope(&format!("window:{window}"));
363        self.animation_values = self.animations.snapshot(self.clock.now());
364        self.effects.remove_scope(root);
365        self.signals.remove_scope(root);
366        self.element_refs.remove_scope(root);
367        self.windows.remove(window);
368        self.responsive.remove_window(window);
369        Ok(())
370    }
371
372    pub(crate) fn queue_async(
373        &mut self,
374        deliveries: impl IntoIterator<Item = crate::AsyncDelivery>,
375    ) {
376        self.pending_async.extend(deliveries);
377    }
378
379    pub(crate) fn queue_suspended_async(
380        &mut self,
381        deliveries: impl IntoIterator<Item = crate::AsyncDelivery>,
382    ) -> Result<(), AsyncRuntimeError> {
383        for delivery in deliveries {
384            if self.pending_async.len() >= Self::SUSPENDED_DELIVERY_CAPACITY {
385                return Err(AsyncRuntimeError::SuspendedBackpressure {
386                    capacity: Self::SUSPENDED_DELIVERY_CAPACITY,
387                });
388            }
389            self.pending_async.push(delivery);
390        }
391        Ok(())
392    }
393
394    pub(crate) fn suspended_delivery_capacity_remaining(&self) -> usize {
395        Self::SUSPENDED_DELIVERY_CAPACITY.saturating_sub(self.pending_async.len())
396    }
397
398    pub(crate) fn cancel_async_scope(&mut self, scope: &AsyncScope) -> Result<(), AssetError> {
399        self.assets.cancel_scope(scope)?;
400        self.tasks.cancel_scope(scope);
401        self.subscriptions.cancel_scope(scope);
402        self.pending_async
403            .retain(|delivery| &delivery.scope != scope);
404        self.trace_subscription_closures();
405        Ok(())
406    }
407
408    pub(crate) fn take_window_async(
409        &mut self,
410        window: &str,
411        root: &ComponentInstancePath,
412    ) -> Vec<crate::AsyncDelivery> {
413        let app_owner = self.windows.open_ids().into_iter().next();
414        let pending = std::mem::take(&mut self.pending_async);
415        let (accepted, retained) =
416            pending
417                .into_iter()
418                .partition(|delivery| match &delivery.scope {
419                    AsyncScope::App => app_owner.as_deref() == Some(window),
420                    AsyncScope::Window(id) => id == window,
421                    AsyncScope::Component(_) | AsyncScope::Effect { .. } => {
422                        delivery.scope.is_within_component(root)
423                    }
424                });
425        self.pending_async = retained;
426        accepted
427    }
428
429    pub(crate) fn discard_component_async_before_generation(
430        &mut self,
431        root: &ComponentInstancePath,
432        generation: crate::ScriptGeneration,
433    ) {
434        self.pending_async.retain(|delivery| {
435            !delivery.scope.is_within_component(root)
436                || delivery.callback.generation() == generation
437        });
438    }
439
440    pub(crate) fn take_window_element_commands(
441        &mut self,
442        window: &str,
443    ) -> Vec<crate::element_ref::ElementCommand> {
444        let commands = std::mem::take(&mut self.pending_element_commands);
445        let (selected, retained) = commands
446            .into_iter()
447            .partition(|command| command.window() == window);
448        self.pending_element_commands = retained;
449        selected
450    }
451
452    pub(crate) fn has_window_dirty(&self, root: &ComponentInstancePath) -> bool {
453        self.dirty.iter().any(|path| path.is_within(root))
454    }
455
456    pub(crate) fn flush_geometry_dependencies(&mut self) {
457        self.dirty.extend(self.geometry.take_dirty());
458        for presentation in self.presentations.values() {
459            self.dirty.extend(presentation.geometry.take_dirty());
460        }
461    }
462
463    pub(crate) fn take_window_dirty_components(
464        &mut self,
465        root: &ComponentInstancePath,
466    ) -> BTreeSet<ComponentInstancePath> {
467        let all = std::mem::take(&mut self.dirty);
468        let (selected, retained) = all.into_iter().partition(|path| path.is_within(root));
469        self.dirty = retained;
470        selected
471    }
472
473    pub(crate) fn drain_pending_actions(&mut self) -> Vec<ActionInvocation> {
474        std::mem::take(&mut self.pending_actions)
475    }
476
477    pub(crate) fn drain_pending_events(&mut self) -> Vec<PendingEvent> {
478        std::mem::take(&mut self.pending_events)
479    }
480
481    pub(crate) fn has_pending_dispatch(&self) -> bool {
482        !self.pending_actions.is_empty() || !self.pending_events.is_empty()
483    }
484
485    pub(crate) fn has_virtual_requests(&self) -> bool {
486        !self.virtual_requests.is_empty()
487    }
488
489    pub(crate) fn component_event_handler(
490        &self,
491        component: &ComponentInstancePath,
492        event: &str,
493    ) -> Option<ScriptCallback> {
494        self.component_event_handlers
495            .get(&(component.clone(), event.to_owned()))
496            .cloned()
497    }
498
499    pub(crate) fn reset_component_readers(&mut self, component: &ComponentInstancePath) {
500        self.stores.reset_reader(component);
501        self.native_collections.reset_reader(component);
502        self.native_documents.reset_reader(component);
503        self.environment_dependencies.reset_reader(component);
504    }
505
506    pub(crate) fn component_event_handlers_in_scope(
507        &self,
508        root: &ComponentInstancePath,
509    ) -> BTreeMap<(ComponentInstancePath, String), ScriptCallback> {
510        self.component_event_handlers
511            .iter()
512            .filter(|((path, _), _)| path.is_within(root))
513            .map(|(id, callback)| (id.clone(), callback.clone()))
514            .collect()
515    }
516
517    pub(crate) fn replace_component_event_handlers(
518        &mut self,
519        root: &ComponentInstancePath,
520        handlers: BTreeMap<(ComponentInstancePath, String), ScriptCallback>,
521    ) {
522        self.component_event_handlers
523            .retain(|(path, _), _| !path.is_within(root));
524        self.component_event_handlers.extend(handlers);
525    }
526
527    pub(crate) fn mark_all_windows_dirty(&mut self) {
528        self.dirty.extend(
529            self.windows
530                .open_ids()
531                .into_iter()
532                .map(|window| ComponentInstancePath::root("App", window)),
533        );
534    }
535
536    pub(crate) fn mark_dirty(
537        &mut self,
538        components: impl IntoIterator<Item = ComponentInstancePath>,
539    ) {
540        self.dirty.extend(components);
541    }
542
543    pub(crate) fn mark_all_windows_repaint(&mut self) {
544        self.repaint_windows.extend(self.windows.open_ids());
545    }
546
547    pub(crate) fn mark_window_repaint(&mut self, window: impl Into<String>) {
548        self.repaint_windows.insert(window.into());
549    }
550
551    pub(crate) fn take_window_repaint(&mut self, window: &str) -> bool {
552        self.repaint_windows.remove(window)
553    }
554
555    pub(crate) fn reconcile_component_lifetimes(
556        &mut self,
557        root: &ComponentInstancePath,
558        active: &BTreeSet<ComponentInstancePath>,
559        previous: &BTreeSet<ComponentInstancePath>,
560    ) -> Result<(), AssetError> {
561        for removed in previous
562            .iter()
563            .filter(|path| path.is_within(root) && *path != root && !active.contains(*path))
564        {
565            self.tasks.cancel_component_scope(removed);
566            self.subscriptions.cancel_component_scope(removed);
567            self.timers.cancel_component_scope(removed);
568            self.assets.cancel_component_scope(removed)?;
569            self.actions.remove_component_scope(removed);
570        }
571        self.pending_async.retain(|delivery| {
572            delivery
573                .scope
574                .component()
575                .is_none_or(|path| !path.is_within(root) || active.contains(path))
576        });
577        self.pending_actions.retain(|action| {
578            !action
579                .callback
580                .component()
581                .is_some_and(|path| path.is_within(root) && !active.contains(path))
582        });
583        self.pending_events.retain(|event| {
584            !event.target.is_within(root) || active.contains(&event.target) || event.target == *root
585        });
586        self.stores.retain_reader_scope(root, active);
587        self.native_collections.retain_reader_scope(root, active);
588        self.native_documents.retain_reader_scope(root, active);
589        self.environment_dependencies.retain_scope(root, active);
590        self.dirty
591            .retain(|path| !path.is_within(root) || path == root || active.contains(path));
592        self.component_incarnations
593            .retain(|path, _| !path.is_within(root) || path == root || active.contains(path));
594        Ok(())
595    }
596
597    #[must_use]
598    pub fn drain_batch(&mut self) -> UiMutationBatch {
599        UiMutationBatch {
600            dirty: std::mem::take(&mut self.dirty),
601            events: std::mem::take(&mut self.pending_events),
602            actions: std::mem::take(&mut self.pending_actions),
603        }
604    }
605
606    #[must_use]
607    pub fn dirty_components(&self) -> &BTreeSet<ComponentInstancePath> {
608        &self.dirty
609    }
610
611    pub(crate) fn snapshot(&self) -> Result<UiStateSnapshot, AssetError> {
612        Ok(UiStateSnapshot {
613            runtime_id: self.runtime_id,
614            transaction_depth: self.transaction_depth,
615            component_state: self.component_state.clone(),
616            stores: self.stores.clone(),
617            native_collections: self.native_collections.clone(),
618            native_documents: self.native_documents.clone(),
619            actions: self.actions.clone(),
620            component_event_handlers: self.component_event_handlers.clone(),
621            component_incarnations: self.component_incarnations.clone(),
622            next_component_incarnation: self.next_component_incarnation,
623            dirty: self.dirty.clone(),
624            pending_events: self.pending_events.clone(),
625            pending_actions: self.pending_actions.clone(),
626            pending_async: self.pending_async.clone(),
627            pending_element_commands: self.pending_element_commands.clone(),
628            locale: self.locale.clone(),
629            theme: self.theme.clone(),
630            component_styles: self.component_styles.clone(),
631            component_style_generation: self.component_style_generation,
632            animations: self.animations.clone(),
633            effects: self.effects.clone(),
634            signals: self.signals.clone(),
635            element_refs: self.element_refs.clone(),
636            geometry: self.geometry.snapshot(),
637            pointer_capture: self.pointer_capture.snapshot(),
638            presentations: self
639                .presentations
640                .iter()
641                .map(|(view, state)| (view.clone(), state.snapshot()))
642                .collect(),
643            budgets: self.budgets.clone(),
644            virtual_requests: self.virtual_requests.snapshot(),
645            animation_values: self.animation_values.clone(),
646            windows: self.windows.clone(),
647            responsive: self.responsive.clone(),
648            environment_dependencies: self.environment_dependencies.clone(),
649            repaint_windows: self.repaint_windows.clone(),
650            tasks: self.tasks.snapshot(),
651            subscriptions: self.subscriptions.snapshot(),
652            timers: self.timers.clone(),
653            image_decodes: self.assets.decode_snapshot()?,
654        })
655    }
656
657    pub(crate) fn restore(&mut self, snapshot: UiStateSnapshot) -> Result<(), UiTransactionError> {
658        if self.runtime_id != snapshot.runtime_id {
659            return Err(UiTransactionError::ForeignSnapshot);
660        }
661        let expected_depth = snapshot.transaction_depth.saturating_add(1);
662        if self.transaction_depth != expected_depth {
663            return Err(UiTransactionError::Unbalanced {
664                expected: expected_depth,
665                actual: self.transaction_depth,
666            });
667        }
668        self.tasks.restore(snapshot.tasks);
669        self.subscriptions.restore(snapshot.subscriptions);
670        self.timers = snapshot.timers;
671        let decode_result = self.assets.restore_decode_snapshot(snapshot.image_decodes);
672        self.component_state = snapshot.component_state;
673        self.stores = snapshot.stores;
674        self.native_collections = snapshot.native_collections;
675        self.native_documents = snapshot.native_documents;
676        self.actions = snapshot.actions;
677        self.component_event_handlers = snapshot.component_event_handlers;
678        self.component_incarnations = snapshot.component_incarnations;
679        self.next_component_incarnation = snapshot.next_component_incarnation;
680        self.dirty = snapshot.dirty;
681        self.pending_events = snapshot.pending_events;
682        self.pending_actions = snapshot.pending_actions;
683        self.pending_async = snapshot.pending_async;
684        self.pending_element_commands = snapshot.pending_element_commands;
685        self.locale = snapshot.locale;
686        self.theme = snapshot.theme;
687        self.component_styles = snapshot.component_styles;
688        self.component_style_generation = snapshot.component_style_generation;
689        self.animations = snapshot.animations;
690        self.effects = snapshot.effects;
691        self.signals = snapshot.signals;
692        self.element_refs = snapshot.element_refs;
693        self.geometry.restore(snapshot.geometry);
694        self.pointer_capture.restore(snapshot.pointer_capture);
695        self.presentations
696            .retain(|view, _| snapshot.presentations.contains_key(view));
697        for (view, presentation) in snapshot.presentations {
698            let state = self.presentations.entry(view).or_default();
699            state.geometry.restore(presentation.geometry);
700            state.pointer_capture.restore(presentation.pointer_capture);
701        }
702        self.budgets = snapshot.budgets;
703        self.virtual_requests.restore(snapshot.virtual_requests);
704        self.animation_values = snapshot.animation_values;
705        self.windows = snapshot.windows;
706        self.responsive = snapshot.responsive;
707        self.environment_dependencies = snapshot.environment_dependencies;
708        self.repaint_windows = snapshot.repaint_windows;
709        self.transaction_depth = snapshot.transaction_depth;
710        decode_result.map_err(UiTransactionError::from)
711    }
712
713    /// Begin an explicit rollback-capable runtime transaction.
714    ///
715    /// Cancellation of existing tasks, subscriptions, and image decodes remains
716    /// provisional until commit, so rollback restores their delivery ability.
717    ///
718    /// # Errors
719    ///
720    /// Returns an asset-registry error while decode state is in use.
721    pub fn begin_transaction(&mut self) -> Result<UiStateSnapshot, UiTransactionError> {
722        let snapshot = self.snapshot()?;
723        let _ = self.assets.begin_transaction()?;
724        self.tasks.begin_transaction();
725        self.subscriptions.begin_transaction();
726        self.transaction_depth = self.transaction_depth.saturating_add(1);
727        Ok(snapshot)
728    }
729
730    /// Commit a transaction and publish deferred resource cancellation when the
731    /// outermost nested transaction completes.
732    ///
733    /// # Errors
734    ///
735    /// Returns an asset-registry error or an unmatched-commit error.
736    pub fn commit_transaction(&mut self) -> Result<(), UiTransactionError> {
737        if self.transaction_depth == 0 {
738            return Err(UiTransactionError::Unbalanced {
739                expected: 1,
740                actual: 0,
741            });
742        }
743        self.assets.commit_transaction()?;
744        self.tasks.commit_transaction();
745        self.subscriptions.commit_transaction();
746        self.transaction_depth -= 1;
747        Ok(())
748    }
749
750    /// Roll back a transaction to its checkpoint.
751    ///
752    /// # Errors
753    ///
754    /// Returns an asset-registry error, foreign-checkpoint error, or an
755    /// unbalanced transaction-depth error.
756    pub fn rollback_transaction(
757        &mut self,
758        snapshot: UiStateSnapshot,
759    ) -> Result<(), UiTransactionError> {
760        self.restore(snapshot)
761    }
762}
763
764#[derive(Debug, Default)]
765struct ViewPresentationState {
766    geometry: crate::GeometryRegistry,
767    pointer_capture: crate::PointerCaptureRegistry,
768}
769
770impl ViewPresentationState {
771    fn snapshot(&self) -> ViewPresentationSnapshot {
772        ViewPresentationSnapshot {
773            geometry: self.geometry.snapshot(),
774            pointer_capture: self.pointer_capture.snapshot(),
775        }
776    }
777}
778
779#[derive(Clone, Debug)]
780struct ViewPresentationSnapshot {
781    geometry: crate::geometry::GeometrySnapshot,
782    pointer_capture: BTreeMap<u64, crate::NodeId>,
783}
784
785#[derive(Debug, Error)]
786pub enum UiTransactionError {
787    #[error("transaction snapshot belongs to another UiRuntimeState")]
788    ForeignSnapshot,
789    #[error("unbalanced runtime transaction depth: expected {expected}, got {actual}")]
790    Unbalanced { expected: usize, actual: usize },
791    #[error(transparent)]
792    Asset(#[from] AssetError),
793}
794
795#[derive(Clone)]
796pub struct UiStateSnapshot {
797    runtime_id: RuntimeStateId,
798    transaction_depth: usize,
799    component_state: StateStore,
800    stores: StoreRegistry,
801    native_collections: crate::NativeCollectionRegistry,
802    native_documents: crate::NativeTextDocumentRegistry,
803    actions: ActionRegistry,
804    component_event_handlers: BTreeMap<(ComponentInstancePath, String), ScriptCallback>,
805    component_incarnations: BTreeMap<ComponentInstancePath, ComponentIncarnation>,
806    next_component_incarnation: u64,
807    dirty: BTreeSet<ComponentInstancePath>,
808    pending_events: Vec<PendingEvent>,
809    pending_actions: Vec<ActionInvocation>,
810    pending_async: Vec<crate::AsyncDelivery>,
811    pending_element_commands: Vec<crate::element_ref::ElementCommand>,
812    locale: Option<LocaleManager>,
813    theme: Option<ThemeManager>,
814    component_styles: crate::ComponentStyleSheet,
815    component_style_generation: u64,
816    animations: AnimationRuntime,
817    effects: crate::EffectRegistry,
818    signals: crate::SignalRegistry,
819    element_refs: crate::ElementRefRegistry,
820    geometry: crate::geometry::GeometrySnapshot,
821    pointer_capture: BTreeMap<u64, crate::NodeId>,
822    presentations: BTreeMap<String, ViewPresentationSnapshot>,
823    budgets: crate::RuntimeBudgets,
824    virtual_requests: crate::virtual_list::VirtualRequestSnapshot,
825    animation_values: BTreeMap<AnimationKey, f64>,
826    windows: WindowCommandRegistry,
827    responsive: ResponsiveRuntime,
828    environment_dependencies: crate::environment_dependency::EnvironmentDependencyRegistry,
829    repaint_windows: BTreeSet<String>,
830    tasks: crate::async_runtime::TaskRegistrySnapshot,
831    subscriptions: crate::async_runtime::SubscriptionRegistrySnapshot,
832    timers: crate::TimerRegistry,
833    image_decodes: crate::asset::ImageDecodeSnapshot,
834}
835
836impl UiStateSnapshot {
837    pub(crate) const fn component_state(&self) -> &StateStore {
838        &self.component_state
839    }
840}
841
842#[derive(Clone, Debug)]
843pub struct PendingEvent {
844    pub target: ComponentInstancePath,
845    pub event: UiEvent,
846}
847
848#[derive(Clone, Debug, Default)]
849pub struct UiMutationBatch {
850    pub dirty: BTreeSet<ComponentInstancePath>,
851    pub events: Vec<PendingEvent>,
852    pub actions: Vec<ActionInvocation>,
853}
854
855#[derive(Clone, Debug)]
856pub struct UiContext {
857    runtime: Rc<RefCell<UiRuntimeState>>,
858    component: ComponentInstancePath,
859    incarnation: ComponentIncarnation,
860    window: Option<String>,
861    view: Option<String>,
862    phase: ExecutionPhase,
863    events: BTreeMap<String, EventSchema>,
864    generation: ScriptGeneration,
865    native_context: Option<crate::invocation::ScriptInvocationContext>,
866    async_scope: Option<AsyncScope>,
867    component_style: Option<crate::Style>,
868    component_part_styles: BTreeMap<String, crate::Style>,
869    global_component_part_styles: BTreeMap<String, crate::Style>,
870    non_reusable_render_reads: Rc<RefCell<BTreeSet<ComponentInstancePath>>>,
871    event_target: Option<crate::GeometryBounds>,
872}
873
874impl UiContext {
875    #[must_use]
876    pub fn new(
877        runtime: Rc<RefCell<UiRuntimeState>>,
878        component: ComponentInstancePath,
879        window: Option<String>,
880        phase: ExecutionPhase,
881        events: BTreeMap<String, EventSchema>,
882    ) -> Self {
883        if let Some(window) = window.as_deref()
884            && let Ok(mut runtime) = runtime.try_borrow_mut()
885        {
886            runtime.ensure_presentation(window);
887        }
888        let incarnation = runtime
889            .try_borrow_mut()
890            .map(|mut runtime| runtime.ensure_component_incarnation(&component))
891            .unwrap_or_default();
892        let context = Self {
893            runtime,
894            component,
895            incarnation,
896            window,
897            view: None,
898            phase,
899            events,
900            generation: ScriptGeneration::default(),
901            native_context: None,
902            async_scope: None,
903            component_style: None,
904            component_part_styles: BTreeMap::new(),
905            global_component_part_styles: BTreeMap::new(),
906            non_reusable_render_reads: Rc::new(RefCell::new(BTreeSet::new())),
907            event_target: None,
908        };
909        if phase == ExecutionPhase::Render
910            && let Ok(mut runtime) = context.runtime.try_borrow_mut()
911        {
912            runtime.reset_component_readers(&context.component);
913        }
914        context
915    }
916
917    #[must_use]
918    pub fn with_generation(mut self, generation: ScriptGeneration) -> Self {
919        self.generation = generation;
920        self
921    }
922
923    #[must_use]
924    pub fn with_view_id(mut self, view: impl Into<String>) -> Self {
925        let view = view.into();
926        if let Ok(mut runtime) = self.runtime.try_borrow_mut() {
927            runtime.ensure_presentation(&view);
928        }
929        self.view = Some(view);
930        self
931    }
932
933    #[must_use]
934    pub(crate) fn with_optional_view_id(mut self, view: Option<String>) -> Self {
935        if let Some(view) = view.as_deref()
936            && let Ok(mut runtime) = self.runtime.try_borrow_mut()
937        {
938            runtime.ensure_presentation(view);
939        }
940        self.view = view;
941        self
942    }
943
944    pub(crate) fn for_component(
945        &self,
946        component: ComponentInstancePath,
947        events: BTreeMap<String, EventSchema>,
948    ) -> Self {
949        let incarnation = self
950            .runtime
951            .try_borrow_mut()
952            .map(|mut runtime| runtime.ensure_component_incarnation(&component))
953            .unwrap_or_default();
954        let context = Self {
955            runtime: Rc::clone(&self.runtime),
956            component,
957            incarnation,
958            window: self.window.clone(),
959            view: self.view.clone(),
960            phase: self.phase,
961            events,
962            generation: self.generation,
963            native_context: self.native_context.clone(),
964            async_scope: self.async_scope.clone(),
965            component_style: self.component_style.clone(),
966            component_part_styles: self.component_part_styles.clone(),
967            global_component_part_styles: self.global_component_part_styles.clone(),
968            non_reusable_render_reads: Rc::clone(&self.non_reusable_render_reads),
969            event_target: self.event_target,
970        };
971        if context.phase == ExecutionPhase::Render
972            && let Ok(mut runtime) = context.runtime.try_borrow_mut()
973        {
974            runtime.reset_component_readers(&context.component);
975        }
976        context
977    }
978
979    pub(crate) fn with_native_context(
980        mut self,
981        native_context: Option<crate::invocation::ScriptInvocationContext>,
982    ) -> Self {
983        self.native_context = native_context;
984        self
985    }
986
987    pub(crate) const fn with_event_target(mut self, target: Option<crate::GeometryBounds>) -> Self {
988        self.event_target = target;
989        self
990    }
991
992    pub(crate) fn with_async_scope(mut self, scope: AsyncScope) -> Self {
993        self.async_scope = Some(scope);
994        self
995    }
996
997    pub(crate) fn with_component_styles(
998        mut self,
999        global_part_styles: BTreeMap<String, crate::Style>,
1000        style: Option<crate::Style>,
1001        part_styles: BTreeMap<String, crate::Style>,
1002    ) -> Self {
1003        self.global_component_part_styles = global_part_styles;
1004        self.component_style = style;
1005        self.component_part_styles = part_styles;
1006        self
1007    }
1008
1009    pub(crate) fn reset_non_reusable_render_reads(&self) {
1010        self.non_reusable_render_reads.borrow_mut().clear();
1011    }
1012
1013    pub(crate) fn component_render_is_reusable(&self) -> bool {
1014        !self
1015            .non_reusable_render_reads
1016            .borrow()
1017            .contains(&self.component)
1018    }
1019
1020    fn mark_non_reusable_render_read(&self) {
1021        if self.phase == ExecutionPhase::Render {
1022            self.non_reusable_render_reads
1023                .borrow_mut()
1024                .insert(self.component.clone());
1025        }
1026    }
1027
1028    fn resolve_component_style(&self, part: &str, mut base: crate::Style) -> crate::Style {
1029        if let Some(style) = self.global_component_part_styles.get(part) {
1030            base = base.merged(style);
1031        }
1032        if part == "root"
1033            && let Some(style) = &self.component_style
1034        {
1035            base = base.merged(style);
1036        }
1037        if let Some(style) = self.component_part_styles.get(part) {
1038            base = base.merged(style);
1039        }
1040        base
1041    }
1042
1043    fn scoped_callback(&self, function: FnPtr) -> Result<ScriptCallback, UiContextError> {
1044        let mut callback = ScriptCallback::try_from_fn_ptr(function, self.generation)?;
1045        callback.bind_component_scope_if_unset(
1046            &self.component,
1047            self.incarnation,
1048            self.events.clone(),
1049        );
1050        if let Some(context) = &self.native_context {
1051            callback.bind_native_context_if_unset(context.clone());
1052        }
1053        Ok(callback)
1054    }
1055
1056    #[must_use]
1057    pub fn runtime(&self) -> &Rc<RefCell<UiRuntimeState>> {
1058        &self.runtime
1059    }
1060
1061    pub(crate) fn component_path(&self) -> &ComponentInstancePath {
1062        &self.component
1063    }
1064
1065    pub(crate) const fn component_incarnation(&self) -> ComponentIncarnation {
1066        self.incarnation
1067    }
1068
1069    pub(crate) fn event_schemas(&self) -> &BTreeMap<String, EventSchema> {
1070        &self.events
1071    }
1072
1073    pub(crate) fn native_context(&self) -> Option<&crate::invocation::ScriptInvocationContext> {
1074        self.native_context.as_ref()
1075    }
1076
1077    /// Read declared local component state.
1078    ///
1079    /// # Errors
1080    ///
1081    /// Returns [`UiContextError`] for a poisoned runtime, unknown instance, or
1082    /// unknown state field.
1083    pub fn get_state(&self, field: &str) -> Result<UiValue, UiContextError> {
1084        self.runtime
1085            .try_borrow()
1086            .map_err(|_| UiContextError::Borrowed)?
1087            .component_state
1088            .get(&self.component, field)
1089            .cloned()
1090            .ok_or_else(|| UiContextError::UnknownState {
1091                component: self.component.clone(),
1092                field: field.to_owned(),
1093            })
1094    }
1095
1096    /// Read one existing nested local-state path.
1097    ///
1098    /// Local state remains a component-scoped dependency: this accessor avoids
1099    /// copying an entire collection into Rhai but does not create a smaller
1100    /// rerender boundary than the owning component.
1101    ///
1102    /// # Errors
1103    ///
1104    /// Returns state, borrow, or nested-path errors.
1105    pub fn get_state_path(
1106        &self,
1107        field: &str,
1108        path: &UiValuePath,
1109    ) -> Result<UiValue, UiContextError> {
1110        Ok(self.get_state(field)?.get_path(path)?.clone())
1111    }
1112
1113    /// Queue a schema-checked local state mutation.
1114    ///
1115    /// # Errors
1116    ///
1117    /// Returns [`UiContextError::MutationDuringRender`] in `view`, plus state
1118    /// and value-conversion errors.
1119    pub fn set_state(&self, field: &str, value: Dynamic) -> Result<(), UiContextError> {
1120        self.require_mutation()?;
1121        let value = UiValue::from_dynamic(value)?;
1122        let mut runtime = self
1123            .runtime
1124            .try_borrow_mut()
1125            .map_err(|_| UiContextError::Borrowed)?;
1126        let sensitive = runtime.component_state.is_sensitive(&self.component, field);
1127        let changed = runtime
1128            .component_state
1129            .set(&self.component, field, value.clone())?;
1130        if changed {
1131            runtime.dirty.insert(self.component.clone());
1132        }
1133        runtime.traces.push(
1134            crate::RuntimeTraceKind::State,
1135            self.component.to_string(),
1136            format!("set {field}"),
1137            Some(value),
1138            sensitive,
1139        );
1140        Ok(())
1141    }
1142
1143    /// Replace one existing nested local-state path.
1144    ///
1145    /// # Errors
1146    ///
1147    /// Returns phase, state, conversion, borrow, or nested-path errors. The
1148    /// complete resulting field is checked against its declared schema.
1149    pub fn set_state_path(
1150        &self,
1151        field: &str,
1152        path: &UiValuePath,
1153        value: Dynamic,
1154    ) -> Result<(), UiContextError> {
1155        self.require_mutation()?;
1156        let value = UiValue::from_dynamic(value)?;
1157        let mut runtime = self
1158            .runtime
1159            .try_borrow_mut()
1160            .map_err(|_| UiContextError::Borrowed)?;
1161        let mut root = runtime
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        root.set_path(path, value.clone())?;
1170        let sensitive = runtime.component_state.is_sensitive(&self.component, field);
1171        if runtime.component_state.set(&self.component, field, root)? {
1172            runtime.dirty.insert(self.component.clone());
1173        }
1174        runtime.traces.push(
1175            crate::RuntimeTraceKind::State,
1176            self.component.to_string(),
1177            format!("set nested {field}"),
1178            Some(value),
1179            sensitive,
1180        );
1181        Ok(())
1182    }
1183
1184    /// Read a native hot value without establishing a component dependency.
1185    ///
1186    /// # Errors
1187    ///
1188    /// Returns a stale-signal or runtime borrow error.
1189    pub fn get_signal(&self, signal: &crate::NativeSignal) -> Result<Dynamic, UiContextError> {
1190        self.mark_non_reusable_render_read();
1191        Ok(self
1192            .runtime
1193            .try_borrow()
1194            .map_err(|_| UiContextError::Borrowed)?
1195            .signals
1196            .read(signal)?
1197            .into_dynamic())
1198    }
1199
1200    /// Update a native hot value without dirtying a formal component.
1201    ///
1202    /// # Errors
1203    ///
1204    /// Returns during render, for a stale signal, a type mismatch, or an
1205    /// unsupported value.
1206    pub fn set_signal(
1207        &self,
1208        signal: &crate::NativeSignal,
1209        value: Dynamic,
1210    ) -> Result<(), UiContextError> {
1211        self.require_mutation()?;
1212        let value = crate::SignalValue::from_dynamic_for_kind(value, signal.id().kind())?;
1213        let mut runtime = self
1214            .runtime
1215            .try_borrow_mut()
1216            .map_err(|_| UiContextError::Borrowed)?;
1217        let changed =
1218            runtime
1219                .signals
1220                .write_from(signal, value.clone(), crate::SignalWriter::Script)?;
1221        runtime.traces.push(
1222            crate::RuntimeTraceKind::Signal,
1223            signal.id().component().to_string(),
1224            format!(
1225                "set {} ({}){}",
1226                signal.id().key(),
1227                signal.id().kind().as_str(),
1228                if changed { "" } else { " unchanged" }
1229            ),
1230            None,
1231            false,
1232        );
1233        Ok(())
1234    }
1235
1236    /// Resolve and read a component-local signal key without tracking a dependency.
1237    ///
1238    /// # Errors
1239    ///
1240    /// Returns when the key is not mounted or runtime state is borrowed.
1241    pub fn get_signal_by_key(&self, key: &str) -> Result<Dynamic, UiContextError> {
1242        self.mark_non_reusable_render_read();
1243        let runtime = self
1244            .runtime
1245            .try_borrow()
1246            .map_err(|_| UiContextError::Borrowed)?;
1247        let signal = runtime.signals.resolve(&self.component, key)?;
1248        Ok(runtime.signals.read(&signal)?.into_dynamic())
1249    }
1250
1251    /// Resolve an optional-float signal owned by the nearest parent component.
1252    /// This is intended for retained render scopes, such as virtual collection
1253    /// item renderers, that consume a signal declared by their owner while the
1254    /// owner's initial render transaction is still in progress.
1255    ///
1256    /// # Errors
1257    ///
1258    /// Returns a borrow, identity, type, or unknown-signal error.
1259    pub(crate) fn parent_optional_float_signal_ref(
1260        &self,
1261        key: &str,
1262    ) -> Result<crate::NativeSignal, UiContextError> {
1263        let runtime = self
1264            .runtime
1265            .try_borrow()
1266            .map_err(|_| UiContextError::Borrowed)?;
1267        let mut scope = self.component.parent();
1268        while let Some(component) = scope {
1269            if let Ok(signal) = runtime.signals.resolve(&component, key) {
1270                if signal.id().kind() == crate::SignalKind::OptionalFloat {
1271                    return Ok(signal);
1272                }
1273                return Err(crate::SignalError::TypeMismatch {
1274                    expected: crate::SignalKind::OptionalFloat,
1275                    actual: signal.id().kind(),
1276                }
1277                .into());
1278            }
1279            if let Some(incarnation) = runtime.component_incarnation(&component) {
1280                let id = crate::SignalId::new_scoped(
1281                    component,
1282                    incarnation,
1283                    key,
1284                    crate::SignalKind::OptionalFloat,
1285                )?;
1286                return Ok(crate::NativeSignal::new(id));
1287            }
1288            scope = component.parent();
1289        }
1290        Err(crate::SignalError::UnknownKey {
1291            component: self.component.clone(),
1292            key: key.to_owned(),
1293        }
1294        .into())
1295    }
1296
1297    /// Resolve and write a component-local signal key.
1298    ///
1299    /// # Errors
1300    ///
1301    /// Returns during render, when the key is not mounted, or for a type mismatch.
1302    pub fn set_signal_by_key(&self, key: &str, value: Dynamic) -> Result<(), UiContextError> {
1303        self.require_mutation()?;
1304        let signal = self
1305            .runtime
1306            .try_borrow()
1307            .map_err(|_| UiContextError::Borrowed)?
1308            .signals
1309            .resolve(&self.component, key)?;
1310        self.set_signal(&signal, value)
1311    }
1312
1313    /// Read last committed layout/visual geometry through a stable element ref.
1314    ///
1315    /// The exact node becomes a component dependency. The first render returns
1316    /// null until GPUI has committed prepaint, which then dirties the reader.
1317    ///
1318    /// # Errors
1319    ///
1320    /// Returns for stale refs, unavailable geometry, or runtime borrow conflicts.
1321    pub fn element_bounds(&self, reference: &crate::ElementRef) -> Result<UiValue, UiContextError> {
1322        let mut runtime = self
1323            .runtime
1324            .try_borrow_mut()
1325            .map_err(|_| UiContextError::Borrowed)?;
1326        let node = if self.phase == ExecutionPhase::Render {
1327            runtime
1328                .element_refs
1329                .resolve_and_track_geometry(reference, &self.component)
1330        } else {
1331            Some(runtime.element_refs.resolve(reference)?)
1332        };
1333        let Some(node) = node else {
1334            return Ok(UiValue::Null);
1335        };
1336        let geometry_registry =
1337            runtime.geometry_for(self.view.as_deref().or(self.window.as_deref()));
1338        drop(runtime);
1339        let Some(geometry) = geometry_registry.read_tracked(node, &self.component) else {
1340            return Ok(UiValue::Null);
1341        };
1342        Ok(UiValue::Map(BTreeMap::from([
1343            ("layout".to_owned(), geometry_bounds_value(geometry.layout)),
1344            ("visual".to_owned(), geometry_bounds_value(geometry.visual)),
1345            (
1346                "clip".to_owned(),
1347                geometry.clip.map_or(UiValue::Null, geometry_bounds_value),
1348            ),
1349        ])))
1350    }
1351
1352    /// Resolve and read a committed component-local ref by its stable key.
1353    ///
1354    /// This is intended for event callbacks, which cannot retain the custom
1355    /// [`crate::ElementRef`] value created during render.
1356    ///
1357    /// # Errors
1358    ///
1359    /// Returns for unknown keys, unavailable geometry, or runtime borrow conflicts.
1360    pub fn element_bounds_by_key(&self, key: &str) -> Result<UiValue, UiContextError> {
1361        let reference = self
1362            .runtime
1363            .try_borrow()
1364            .map_err(|_| UiContextError::Borrowed)?
1365            .element_refs
1366            .resolve_key(&self.component, key)?;
1367        self.element_bounds(&reference)
1368    }
1369
1370    /// Read the current handler node's committed visual bounds without
1371    /// establishing a render dependency.
1372    ///
1373    /// This event-time snapshot is `null` for callbacks that were not
1374    /// dispatched from a retained node or whose geometry is unavailable.
1375    ///
1376    /// # Errors
1377    ///
1378    /// Returns [`UiContextError::EventTargetOutsideEvent`] during render,
1379    /// initialization, or disposal.
1380    pub fn event_target_bounds(&self) -> Result<UiValue, UiContextError> {
1381        if self.phase != ExecutionPhase::Event {
1382            return Err(UiContextError::EventTargetOutsideEvent);
1383        }
1384        Ok(self
1385            .event_target
1386            .map_or(UiValue::Null, crate::GeometryBounds::into_value))
1387    }
1388
1389    /// Queue focus for a mounted element ref in the current window.
1390    ///
1391    /// # Errors
1392    ///
1393    /// Returns during render, outside a window, or for a stale ref.
1394    pub fn focus_element(&self, reference: &crate::ElementRef) -> Result<(), UiContextError> {
1395        self.require_mutation()?;
1396        let window = self.window.clone().ok_or(UiContextError::MissingWindow)?;
1397        let mut runtime = self
1398            .runtime
1399            .try_borrow_mut()
1400            .map_err(|_| UiContextError::Borrowed)?;
1401        let node = runtime.element_refs.resolve(reference)?;
1402        runtime
1403            .pending_element_commands
1404            .push(crate::element_ref::ElementCommand::Focus { window, node });
1405        Ok(())
1406    }
1407
1408    /// Resolve and queue focus for a component-local ref key.
1409    ///
1410    /// # Errors
1411    ///
1412    /// Returns the same errors as [`Self::focus_element`] plus unknown keys.
1413    pub fn focus_element_by_key(&self, key: &str) -> Result<(), UiContextError> {
1414        let reference = self
1415            .runtime
1416            .try_borrow()
1417            .map_err(|_| UiContextError::Borrowed)?
1418            .element_refs
1419            .resolve_key(&self.component, key)?;
1420        self.focus_element(&reference)
1421    }
1422
1423    /// Queue a positive visible scroll offset for a retained scroll container.
1424    ///
1425    /// # Errors
1426    ///
1427    /// Returns during render, outside a window, for a stale ref, or for invalid
1428    /// offsets.
1429    pub fn scroll_element_to(
1430        &self,
1431        reference: &crate::ElementRef,
1432        x: f64,
1433        y: f64,
1434    ) -> Result<(), UiContextError> {
1435        self.require_mutation()?;
1436        if !x.is_finite() || !y.is_finite() || x < 0.0 || y < 0.0 {
1437            return Err(UiContextError::InvalidScrollOffset { x, y });
1438        }
1439        let window = self.window.clone().ok_or(UiContextError::MissingWindow)?;
1440        let mut runtime = self
1441            .runtime
1442            .try_borrow_mut()
1443            .map_err(|_| UiContextError::Borrowed)?;
1444        let node = runtime.element_refs.resolve(reference)?;
1445        runtime
1446            .pending_element_commands
1447            .push(crate::element_ref::ElementCommand::ScrollTo { window, node, x, y });
1448        Ok(())
1449    }
1450
1451    /// Resolve and queue scroll for a component-local ref key.
1452    ///
1453    /// # Errors
1454    ///
1455    /// Returns the same errors as [`Self::scroll_element_to`] plus unknown keys.
1456    pub fn scroll_element_to_by_key(
1457        &self,
1458        key: &str,
1459        x: f64,
1460        y: f64,
1461    ) -> Result<(), UiContextError> {
1462        let reference = self
1463            .runtime
1464            .try_borrow()
1465            .map_err(|_| UiContextError::Borrowed)?
1466            .element_refs
1467            .resolve_key(&self.component, key)?;
1468        self.scroll_element_to(&reference, x, y)
1469    }
1470
1471    /// Queue minimal ancestor scrolling that reveals a retained descendant.
1472    ///
1473    /// # Errors
1474    ///
1475    /// Returns during render, outside a window, or for a stale ref.
1476    pub fn scroll_element_into_view(
1477        &self,
1478        reference: &crate::ElementRef,
1479    ) -> Result<(), UiContextError> {
1480        self.require_mutation()?;
1481        let window = self.window.clone().ok_or(UiContextError::MissingWindow)?;
1482        let mut runtime = self
1483            .runtime
1484            .try_borrow_mut()
1485            .map_err(|_| UiContextError::Borrowed)?;
1486        let node = runtime.element_refs.resolve(reference)?;
1487        runtime
1488            .pending_element_commands
1489            .push(crate::element_ref::ElementCommand::ScrollIntoView { window, node });
1490        Ok(())
1491    }
1492
1493    /// Resolve and reveal a component-local ref key.
1494    ///
1495    /// # Errors
1496    ///
1497    /// Returns the same errors as [`Self::scroll_element_into_view`] plus unknown keys.
1498    pub fn scroll_element_into_view_by_key(&self, key: &str) -> Result<(), UiContextError> {
1499        let reference = self
1500            .runtime
1501            .try_borrow()
1502            .map_err(|_| UiContextError::Borrowed)?
1503            .element_refs
1504            .resolve_key(&self.component, key)?;
1505        self.scroll_element_into_view(&reference)
1506    }
1507
1508    /// Read and subscribe to an app-scoped store field.
1509    ///
1510    /// # Errors
1511    ///
1512    /// Returns store or lock errors.
1513    pub fn get_app_store(&self, store: &str, field: &str) -> Result<UiValue, UiContextError> {
1514        let mut runtime = self
1515            .runtime
1516            .try_borrow_mut()
1517            .map_err(|_| UiContextError::Borrowed)?;
1518        Ok(runtime
1519            .stores
1520            .read_tracked(&self.component, &StoreId::app(store), field)?)
1521    }
1522
1523    /// Read one Rust-owned collection and subscribe the current component.
1524    ///
1525    /// The returned value is an opaque collection view: Rhai can pass it to
1526    /// collection-aware components but cannot enumerate and materialize all rows.
1527    ///
1528    /// # Errors
1529    ///
1530    /// Returns collection or borrow errors.
1531    pub fn get_native_collection(
1532        &self,
1533        name: &str,
1534    ) -> Result<crate::NativeCollection, UiContextError> {
1535        let mut runtime = self
1536            .runtime
1537            .try_borrow_mut()
1538            .map_err(|_| UiContextError::Borrowed)?;
1539        Ok(runtime
1540            .native_collections
1541            .read_tracked(&self.component, name)?)
1542    }
1543
1544    /// Read and subscribe to one immutable Host-owned text revision.
1545    ///
1546    /// # Errors
1547    ///
1548    /// Returns document or runtime borrow errors.
1549    pub fn get_native_text_document(
1550        &self,
1551        name: &str,
1552    ) -> Result<crate::NativeTextDocument, UiContextError> {
1553        let mut runtime = self
1554            .runtime
1555            .try_borrow_mut()
1556            .map_err(|_| UiContextError::Borrowed)?;
1557        Ok(runtime
1558            .native_documents
1559            .read_tracked(&self.component, name)?)
1560    }
1561
1562    /// Read and subscribe to one exact app-store path.
1563    ///
1564    /// # Errors
1565    ///
1566    /// Returns store, path, or borrow errors.
1567    pub fn get_app_store_path(
1568        &self,
1569        store: &str,
1570        field: &str,
1571        path: &UiValuePath,
1572    ) -> Result<UiValue, UiContextError> {
1573        let mut runtime = self
1574            .runtime
1575            .try_borrow_mut()
1576            .map_err(|_| UiContextError::Borrowed)?;
1577        Ok(runtime
1578            .stores
1579            .read_path_tracked(&self.component, &StoreId::app(store), field, path)?)
1580    }
1581
1582    /// Queue a schema-checked app store mutation.
1583    ///
1584    /// # Errors
1585    ///
1586    /// Returns phase, conversion, store, or lock errors.
1587    pub fn set_app_store(
1588        &self,
1589        store: &str,
1590        field: &str,
1591        value: Dynamic,
1592    ) -> Result<(), UiContextError> {
1593        self.require_mutation()?;
1594        let value = UiValue::from_dynamic(value)?;
1595        let mut runtime = self
1596            .runtime
1597            .try_borrow_mut()
1598            .map_err(|_| UiContextError::Borrowed)?;
1599        let id = StoreId::app(store);
1600        let sensitive = runtime.stores.is_sensitive(&id, field);
1601        let invalidated = runtime.stores.write(&id, field, value.clone())?;
1602        runtime.dirty.extend(invalidated);
1603        runtime.traces.push(
1604            crate::RuntimeTraceKind::Store,
1605            format!("app:{store}"),
1606            format!("set {field}"),
1607            Some(value),
1608            sensitive,
1609        );
1610        Ok(())
1611    }
1612
1613    /// Replace one exact app-store path and invalidate only affected readers.
1614    ///
1615    /// # Errors
1616    ///
1617    /// Returns phase, conversion, store, path, schema, or borrow errors.
1618    pub fn set_app_store_path(
1619        &self,
1620        store: &str,
1621        field: &str,
1622        path: &UiValuePath,
1623        value: Dynamic,
1624    ) -> Result<(), UiContextError> {
1625        self.require_mutation()?;
1626        let value = UiValue::from_dynamic(value)?;
1627        let mut runtime = self
1628            .runtime
1629            .try_borrow_mut()
1630            .map_err(|_| UiContextError::Borrowed)?;
1631        let id = StoreId::app(store);
1632        let sensitive = runtime.stores.is_sensitive(&id, field);
1633        let invalidated = runtime.stores.write_path(&id, field, path, value.clone())?;
1634        runtime.dirty.extend(invalidated);
1635        runtime.traces.push(
1636            crate::RuntimeTraceKind::Store,
1637            format!("app:{store}"),
1638            format!("set nested {field}"),
1639            Some(value),
1640            sensitive,
1641        );
1642        Ok(())
1643    }
1644
1645    /// Read and subscribe to a store scoped to this context's window.
1646    ///
1647    /// # Errors
1648    ///
1649    /// Returns [`UiContextError::MissingWindow`] outside a window, plus store
1650    /// and lock errors.
1651    pub fn get_window_store(&self, store: &str, field: &str) -> Result<UiValue, UiContextError> {
1652        let window = self.window.as_ref().ok_or(UiContextError::MissingWindow)?;
1653        let mut runtime = self
1654            .runtime
1655            .try_borrow_mut()
1656            .map_err(|_| UiContextError::Borrowed)?;
1657        Ok(runtime
1658            .stores
1659            .read_tracked(&self.component, &StoreId::window(window, store), field)?)
1660    }
1661
1662    /// Read and subscribe to one exact path in the current window's store.
1663    ///
1664    /// # Errors
1665    ///
1666    /// Returns missing-window, store, path, or borrow errors.
1667    pub fn get_window_store_path(
1668        &self,
1669        store: &str,
1670        field: &str,
1671        path: &UiValuePath,
1672    ) -> Result<UiValue, UiContextError> {
1673        let window = self.window.as_ref().ok_or(UiContextError::MissingWindow)?;
1674        let mut runtime = self
1675            .runtime
1676            .try_borrow_mut()
1677            .map_err(|_| UiContextError::Borrowed)?;
1678        Ok(runtime.stores.read_path_tracked(
1679            &self.component,
1680            &StoreId::window(window, store),
1681            field,
1682            path,
1683        )?)
1684    }
1685
1686    /// Queue a schema-checked mutation in the current window's store.
1687    ///
1688    /// # Errors
1689    ///
1690    /// Returns phase, missing-window, conversion, store, or borrow errors.
1691    pub fn set_window_store(
1692        &self,
1693        store: &str,
1694        field: &str,
1695        value: Dynamic,
1696    ) -> Result<(), UiContextError> {
1697        self.require_mutation()?;
1698        let window = self.window.as_ref().ok_or(UiContextError::MissingWindow)?;
1699        let value = UiValue::from_dynamic(value)?;
1700        let mut runtime = self
1701            .runtime
1702            .try_borrow_mut()
1703            .map_err(|_| UiContextError::Borrowed)?;
1704        let id = StoreId::window(window, store);
1705        let sensitive = runtime.stores.is_sensitive(&id, field);
1706        let invalidated = runtime.stores.write(&id, field, value.clone())?;
1707        runtime.dirty.extend(invalidated);
1708        runtime.traces.push(
1709            crate::RuntimeTraceKind::Store,
1710            format!("window:{window}:{store}"),
1711            format!("set {field}"),
1712            Some(value),
1713            sensitive,
1714        );
1715        Ok(())
1716    }
1717
1718    /// Replace one exact path in the current window's store.
1719    ///
1720    /// # Errors
1721    ///
1722    /// Returns phase, missing-window, conversion, store, path, schema, or
1723    /// borrow errors.
1724    pub fn set_window_store_path(
1725        &self,
1726        store: &str,
1727        field: &str,
1728        path: &UiValuePath,
1729        value: Dynamic,
1730    ) -> Result<(), UiContextError> {
1731        self.require_mutation()?;
1732        let window = self.window.as_ref().ok_or(UiContextError::MissingWindow)?;
1733        let value = UiValue::from_dynamic(value)?;
1734        let mut runtime = self
1735            .runtime
1736            .try_borrow_mut()
1737            .map_err(|_| UiContextError::Borrowed)?;
1738        let id = StoreId::window(window, store);
1739        let sensitive = runtime.stores.is_sensitive(&id, field);
1740        let invalidated = runtime.stores.write_path(&id, field, path, value.clone())?;
1741        runtime.dirty.extend(invalidated);
1742        runtime.traces.push(
1743            crate::RuntimeTraceKind::Store,
1744            format!("window:{window}:{store}"),
1745            format!("set nested {field}"),
1746            Some(value),
1747            sensitive,
1748        );
1749        Ok(())
1750    }
1751
1752    /// Emit a declared semantic component event.
1753    ///
1754    /// # Errors
1755    ///
1756    /// Returns phase, schema, conversion, or lock errors.
1757    pub fn emit(&self, event: &str, payload: Dynamic) -> Result<(), UiContextError> {
1758        self.require_mutation()?;
1759        let schema = self
1760            .events
1761            .get(event)
1762            .ok_or_else(|| UiContextError::UnknownEvent(event.to_owned()))?;
1763        schema
1764            .payload
1765            .validate(&payload)
1766            .map_err(UiContextError::InvalidEvent)?;
1767        let payload = UiValue::from_dynamic(payload)?;
1768        let mut runtime = self
1769            .runtime
1770            .try_borrow_mut()
1771            .map_err(|_| UiContextError::Borrowed)?;
1772        runtime.pending_events.push(PendingEvent {
1773            target: self.component.clone(),
1774            event: UiEvent {
1775                name: event.to_owned(),
1776                payload,
1777            },
1778        });
1779        runtime.traces.push(
1780            crate::RuntimeTraceKind::Event,
1781            self.component.to_string(),
1782            format!("emit {event}"),
1783            None,
1784            true,
1785        );
1786        Ok(())
1787    }
1788
1789    /// Dispatch a registered semantic action.
1790    ///
1791    /// # Errors
1792    ///
1793    /// Returns phase, action, conversion, or lock errors.
1794    pub fn dispatch_action(&self, action: &str, payload: Dynamic) -> Result<(), UiContextError> {
1795        self.require_mutation()?;
1796        let id = ActionId::parse(action)?;
1797        let payload = UiValue::from_dynamic(payload)?;
1798        let mut runtime = self
1799            .runtime
1800            .try_borrow_mut()
1801            .map_err(|_| UiContextError::Borrowed)?;
1802        let invocation = runtime.actions.dispatch(&id, payload)?;
1803        runtime.pending_actions.push(invocation);
1804        runtime.traces.push(
1805            crate::RuntimeTraceKind::Action,
1806            self.component.to_string(),
1807            format!("dispatch {action}"),
1808            None,
1809            true,
1810        );
1811        Ok(())
1812    }
1813
1814    /// Register or refresh an app-scoped semantic action callback.
1815    ///
1816    /// # Errors
1817    ///
1818    /// Returns phase, identifier, or borrow errors.
1819    pub fn register_action(&self, action: &str, callback: FnPtr) -> Result<(), UiContextError> {
1820        self.require_mutation()?;
1821        let id = ActionId::parse(action)?;
1822        let callback = self.scoped_callback(callback)?;
1823        self.runtime
1824            .try_borrow_mut()
1825            .map_err(|_| UiContextError::Borrowed)?
1826            .actions
1827            .register_or_replace(id, callback);
1828        Ok(())
1829    }
1830
1831    /// Enable or disable a registered semantic action.
1832    ///
1833    /// # Errors
1834    ///
1835    /// Returns phase, identifier, unknown-action, or borrow errors.
1836    pub fn set_action_enabled(&self, action: &str, enabled: bool) -> Result<(), UiContextError> {
1837        self.require_mutation()?;
1838        let id = ActionId::parse(action)?;
1839        self.runtime
1840            .try_borrow_mut()
1841            .map_err(|_| UiContextError::Borrowed)?
1842            .actions
1843            .set_enabled(&id, enabled)?;
1844        Ok(())
1845    }
1846
1847    /// Invoke a manifest-declared Rust capability.
1848    ///
1849    /// # Errors
1850    ///
1851    /// Returns phase, identifier, schema, handler, conversion, or borrow errors.
1852    pub fn call_capability(
1853        &self,
1854        capability: &str,
1855        method: &str,
1856        input: Dynamic,
1857    ) -> Result<UiValue, UiContextError> {
1858        self.require_mutation()?;
1859        let id = CapabilityId::parse(capability)?;
1860        let input = UiValue::from_dynamic(input)?;
1861        let mut runtime = self
1862            .runtime
1863            .try_borrow_mut()
1864            .map_err(|_| UiContextError::Borrowed)?;
1865        let output = runtime.capabilities.call(&id, method, input)?;
1866        runtime.traces.push(
1867            crate::RuntimeTraceKind::Capability,
1868            self.component.to_string(),
1869            format!("call {capability}.{method}"),
1870            None,
1871            true,
1872        );
1873        Ok(output)
1874    }
1875
1876    fn read_locale<T>(
1877        &self,
1878        read: impl FnOnce(
1879            &LocaleManager,
1880            Option<&str>,
1881            &ComponentInstancePath,
1882        ) -> Result<T, LocaleError>,
1883    ) -> Result<T, UiContextError> {
1884        let mut runtime = self
1885            .runtime
1886            .try_borrow_mut()
1887            .map_err(|_| UiContextError::Borrowed)?;
1888        runtime
1889            .environment_dependencies
1890            .track_locale(self.window.as_deref(), &self.component);
1891        let locale = runtime
1892            .locale
1893            .as_ref()
1894            .ok_or(UiContextError::LocaleUnavailable)?;
1895        Ok(read(locale, self.window.as_deref(), &self.component)?)
1896    }
1897
1898    /// Resolve one localized message for this window/component scope.
1899    ///
1900    /// # Errors
1901    ///
1902    /// Returns [`UiContextError::LocaleUnavailable`], locale, or borrow errors.
1903    pub fn text(&self, key: &str) -> Result<String, UiContextError> {
1904        self.read_locale(|locale, window, component| locale.text(window, Some(component), key))
1905    }
1906
1907    /// Resolve the logical text direction for this component scope.
1908    ///
1909    /// # Errors
1910    ///
1911    /// Returns [`UiContextError::LocaleUnavailable`], locale, or borrow errors.
1912    pub fn text_direction(&self) -> Result<String, UiContextError> {
1913        match self
1914            .read_locale(|locale, window, component| locale.direction(window, Some(component)))?
1915        {
1916            TextDirection::LeftToRight => Ok("ltr".to_owned()),
1917            TextDirection::RightToLeft => Ok("rtl".to_owned()),
1918        }
1919    }
1920
1921    /// Return today's strict ISO date from the host-injected calendar Clock.
1922    ///
1923    /// This is a read-only presentation input and is available during render.
1924    ///
1925    /// # Errors
1926    ///
1927    /// Returns a borrow error when another callback owns the runtime state.
1928    pub fn today(&self) -> Result<String, UiContextError> {
1929        Ok(self
1930            .runtime
1931            .try_borrow()
1932            .map_err(|_| UiContextError::Borrowed)?
1933            .calendar_clock
1934            .today()
1935            .to_iso())
1936    }
1937
1938    /// Format a strict ISO date through the selected locale.
1939    ///
1940    /// # Errors
1941    ///
1942    /// Returns locale, date, style, or runtime borrow errors.
1943    pub fn format_date(&self, iso_date: &str, style: &str) -> Result<String, UiContextError> {
1944        let style = DateStyle::parse(style)?;
1945        self.read_locale(|locale, window, component| {
1946            locale.format_date(window, Some(component), iso_date, style)
1947        })
1948    }
1949
1950    /// Format a strict ISO date with the locale's month/year pattern.
1951    ///
1952    /// # Errors
1953    ///
1954    /// Returns locale, date, or runtime borrow errors.
1955    pub fn format_month_year(&self, iso_date: &str) -> Result<String, UiContextError> {
1956        self.read_locale(|locale, window, component| {
1957            locale.format_month_year(window, Some(component), iso_date)
1958        })
1959    }
1960
1961    /// Return a detached read-only copy of selected calendar metadata.
1962    ///
1963    /// # Errors
1964    ///
1965    /// Returns locale, serialization, or runtime borrow errors.
1966    pub fn calendar_metadata(&self) -> Result<Map, UiContextError> {
1967        let calendar = self.read_locale(|locale, window, component| {
1968            locale.calendar(window, Some(component)).cloned()
1969        })?;
1970        let dynamic = rhai::serde::to_dynamic(calendar)
1971            .map_err(|error| LocaleError::Decode(error.to_string()))?;
1972        Ok(dynamic.cast::<Map>())
1973    }
1974
1975    /// Return a detached read-only copy of selected number metadata.
1976    ///
1977    /// # Errors
1978    ///
1979    /// Returns locale, serialization, or runtime borrow errors.
1980    pub fn number_metadata(&self) -> Result<Map, UiContextError> {
1981        let number = self.read_locale(|locale, window, component| {
1982            locale.number(window, Some(component)).cloned()
1983        })?;
1984        let dynamic = rhai::serde::to_dynamic(number)
1985            .map_err(|error| LocaleError::Decode(error.to_string()))?;
1986        Ok(dynamic.cast::<Map>())
1987    }
1988
1989    /// Format an integer through the selected locale.
1990    ///
1991    /// # Errors
1992    ///
1993    /// Returns locale, option, or runtime borrow errors.
1994    pub fn format_integer(
1995        &self,
1996        value: INT,
1997        options: NumberFormatOptions,
1998    ) -> Result<String, UiContextError> {
1999        self.read_locale(|locale, window, component| {
2000            locale.format_integer(window, Some(component), value, options)
2001        })
2002    }
2003
2004    /// Format a finite decimal number through the selected locale.
2005    ///
2006    /// # Errors
2007    ///
2008    /// Returns locale, option, or runtime borrow errors.
2009    pub fn format_number(
2010        &self,
2011        value: FLOAT,
2012        options: NumberFormatOptions,
2013    ) -> Result<String, UiContextError> {
2014        self.read_locale(|locale, window, component| {
2015            locale.format_number(window, Some(component), value, options)
2016        })
2017    }
2018
2019    /// Change the app locale at runtime without changing component state.
2020    ///
2021    /// # Errors
2022    ///
2023    /// Returns phase, locale, or borrow errors.
2024    pub fn set_locale(&self, locale: &str) -> Result<(), UiContextError> {
2025        self.require_mutation()?;
2026        let mut runtime = self
2027            .runtime
2028            .try_borrow_mut()
2029            .map_err(|_| UiContextError::Borrowed)?;
2030        let changed = {
2031            let locales = runtime
2032                .locale
2033                .as_mut()
2034                .ok_or(UiContextError::LocaleUnavailable)?;
2035            let previous = locales.generation();
2036            locales.set_app(locale)?;
2037            locales.generation() != previous
2038        };
2039        if changed {
2040            let invalidated = runtime.environment_dependencies.invalidate_locale_app();
2041            runtime.dirty.extend(invalidated);
2042            runtime.mark_all_windows_repaint();
2043        }
2044        runtime.traces.push(
2045            crate::RuntimeTraceKind::Locale,
2046            self.component.to_string(),
2047            format!("select {locale}"),
2048            None,
2049            false,
2050        );
2051        Ok(())
2052    }
2053
2054    /// Select an application theme without recompiling scripts or resetting state.
2055    ///
2056    /// # Errors
2057    ///
2058    /// Returns phase, theme availability, selection, or borrow errors.
2059    pub fn set_theme(&self, family: &str, variant: &str) -> Result<(), UiContextError> {
2060        self.set_theme_preference(
2061            ThemeTarget::App,
2062            ThemePreference::Fixed {
2063                selection: ThemeSelection::new(family, variant),
2064            },
2065            format!("select {family}/{variant}"),
2066        )
2067    }
2068
2069    /// Select a theme for this context's window.
2070    ///
2071    /// # Errors
2072    ///
2073    /// Returns phase, missing-window, theme, or borrow errors.
2074    pub fn set_window_theme(&self, family: &str, variant: &str) -> Result<(), UiContextError> {
2075        let window = self.window.clone().ok_or(UiContextError::MissingWindow)?;
2076        self.set_theme_preference(
2077            ThemeTarget::Window(window),
2078            ThemePreference::Fixed {
2079                selection: ThemeSelection::new(family, variant),
2080            },
2081            format!("select window {family}/{variant}"),
2082        )
2083    }
2084
2085    /// Return the stable script-visible ID of the current window.
2086    ///
2087    /// # Errors
2088    ///
2089    /// Returns [`UiContextError::MissingWindow`] outside a window lifecycle.
2090    pub fn window_id(&self) -> Result<String, UiContextError> {
2091        self.window.clone().ok_or(UiContextError::MissingWindow)
2092    }
2093
2094    /// Return the stable identity of the mounted script view.
2095    ///
2096    /// # Errors
2097    ///
2098    /// Returns [`UiContextError::MissingView`] for manually constructed contexts
2099    /// that are not attached to a script view.
2100    pub fn view_id(&self) -> Result<String, UiContextError> {
2101        self.view.clone().ok_or(UiContextError::MissingView)
2102    }
2103
2104    /// Return `compact`, `regular`, or `wide` for the current native window.
2105    ///
2106    /// # Errors
2107    ///
2108    /// Returns [`UiContextError::MissingWindow`] outside a window lifecycle.
2109    pub fn viewport_class(&self) -> Result<String, UiContextError> {
2110        let window = self.window.as_ref().ok_or(UiContextError::MissingWindow)?;
2111        let mut runtime = self
2112            .runtime
2113            .try_borrow_mut()
2114            .map_err(|_| UiContextError::Borrowed)?;
2115        runtime
2116            .environment_dependencies
2117            .track_viewport(window, &self.component);
2118        Ok(runtime.responsive.class(window).as_str().to_owned())
2119    }
2120
2121    /// Queue another native window running the same script entry.
2122    ///
2123    /// # Errors
2124    ///
2125    /// Returns phase, missing-window, validation, duplicate, or queue errors.
2126    pub fn open_window(&self, spec: ScriptWindowSpec) -> Result<(), UiContextError> {
2127        self.require_mutation()?;
2128        let window = self.window.as_ref().ok_or(UiContextError::MissingWindow)?;
2129        let id = spec.id.clone();
2130        let mut runtime = self
2131            .runtime
2132            .try_borrow_mut()
2133            .map_err(|_| UiContextError::Borrowed)?;
2134        runtime.windows.request_open_from(window, spec)?;
2135        runtime.traces.push(
2136            crate::RuntimeTraceKind::Window,
2137            self.component.to_string(),
2138            format!("open {id}"),
2139            None,
2140            false,
2141        );
2142        Ok(())
2143    }
2144
2145    /// Queue activation of a registered native window.
2146    ///
2147    /// # Errors
2148    ///
2149    /// Returns phase, registry, or queue errors.
2150    pub fn focus_window(&self, id: &str) -> Result<(), UiContextError> {
2151        self.require_mutation()?;
2152        let window = self.window.as_ref().ok_or(UiContextError::MissingWindow)?;
2153        let mut runtime = self
2154            .runtime
2155            .try_borrow_mut()
2156            .map_err(|_| UiContextError::Borrowed)?;
2157        runtime.windows.request_focus_from(window, id)?;
2158        runtime.traces.push(
2159            crate::RuntimeTraceKind::Window,
2160            self.component.to_string(),
2161            format!("focus {id}"),
2162            None,
2163            false,
2164        );
2165        Ok(())
2166    }
2167
2168    /// Queue forced close after the script has performed any confirmation UI.
2169    ///
2170    /// # Errors
2171    ///
2172    /// Returns phase, registry, or queue errors.
2173    pub fn close_window(&self, id: &str) -> Result<(), UiContextError> {
2174        self.require_mutation()?;
2175        let window = self.window.as_ref().ok_or(UiContextError::MissingWindow)?;
2176        let mut runtime = self
2177            .runtime
2178            .try_borrow_mut()
2179            .map_err(|_| UiContextError::Borrowed)?;
2180        runtime.windows.request_close_from(window, id)?;
2181        runtime.traces.push(
2182            crate::RuntimeTraceKind::Window,
2183            self.component.to_string(),
2184            format!("close {id}"),
2185            None,
2186            false,
2187        );
2188        Ok(())
2189    }
2190
2191    /// Intercept native close requests with a generation-bound script callback.
2192    /// The callback may show confirmation UI and later call `close_window`.
2193    ///
2194    /// # Errors
2195    ///
2196    /// Returns phase, missing-window, registry, or borrow errors.
2197    pub fn set_close_handler(&self, callback: FnPtr) -> Result<(), UiContextError> {
2198        self.require_mutation()?;
2199        let window = self.window.as_ref().ok_or(UiContextError::MissingWindow)?;
2200        let callback = self.scoped_callback(callback)?;
2201        self.runtime
2202            .try_borrow_mut()
2203            .map_err(|_| UiContextError::Borrowed)?
2204            .windows
2205            .set_close_handler(window, Some(callback))?;
2206        Ok(())
2207    }
2208
2209    /// Remove the current window's close-request interception.
2210    ///
2211    /// # Errors
2212    ///
2213    /// Returns phase, missing-window, registry, or borrow errors.
2214    pub fn clear_close_handler(&self) -> Result<(), UiContextError> {
2215        self.require_mutation()?;
2216        let window = self.window.as_ref().ok_or(UiContextError::MissingWindow)?;
2217        self.runtime
2218            .try_borrow_mut()
2219            .map_err(|_| UiContextError::Borrowed)?
2220            .windows
2221            .set_close_handler(window, None)?;
2222        Ok(())
2223    }
2224
2225    /// Select a locale override for the current window.
2226    ///
2227    /// # Errors
2228    ///
2229    /// Returns phase, missing-window, locale, or borrow errors.
2230    pub fn set_window_locale(&self, locale: &str) -> Result<(), UiContextError> {
2231        self.require_mutation()?;
2232        let window = self.window.clone().ok_or(UiContextError::MissingWindow)?;
2233        let mut runtime = self
2234            .runtime
2235            .try_borrow_mut()
2236            .map_err(|_| UiContextError::Borrowed)?;
2237        let changed = {
2238            let locales = runtime
2239                .locale
2240                .as_mut()
2241                .ok_or(UiContextError::LocaleUnavailable)?;
2242            let previous = locales.generation();
2243            locales.set_window(window.clone(), locale)?;
2244            locales.generation() != previous
2245        };
2246        if changed {
2247            let invalidated = runtime
2248                .environment_dependencies
2249                .invalidate_locale_window(&window);
2250            runtime.dirty.extend(invalidated);
2251            runtime.mark_window_repaint(window.clone());
2252        }
2253        runtime.traces.push(
2254            crate::RuntimeTraceKind::Locale,
2255            self.component.to_string(),
2256            format!("select window {window} locale {locale}"),
2257            None,
2258            false,
2259        );
2260        Ok(())
2261    }
2262
2263    /// Select a locale override for the current component subtree.
2264    ///
2265    /// # Errors
2266    ///
2267    /// Returns phase, locale, or borrow errors.
2268    pub fn set_local_locale(&self, locale: &str) -> Result<(), UiContextError> {
2269        self.require_mutation()?;
2270        let mut runtime = self
2271            .runtime
2272            .try_borrow_mut()
2273            .map_err(|_| UiContextError::Borrowed)?;
2274        let changed = {
2275            let locales = runtime
2276                .locale
2277                .as_mut()
2278                .ok_or(UiContextError::LocaleUnavailable)?;
2279            let previous = locales.generation();
2280            locales.set_scope(self.component.clone(), locale)?;
2281            locales.generation() != previous
2282        };
2283        if changed {
2284            let invalidated = runtime
2285                .environment_dependencies
2286                .invalidate_locale_scope(&self.component);
2287            runtime.dirty.extend(invalidated);
2288            if let Some(window) = &self.window {
2289                runtime.mark_window_repaint(window.clone());
2290            }
2291        }
2292        runtime.traces.push(
2293            crate::RuntimeTraceKind::Locale,
2294            self.component.to_string(),
2295            format!("select subtree locale {locale}"),
2296            None,
2297            false,
2298        );
2299        Ok(())
2300    }
2301
2302    /// Select a theme for the current component subtree.
2303    ///
2304    /// # Errors
2305    ///
2306    /// Returns phase, theme, or borrow errors.
2307    pub fn set_local_theme(&self, family: &str, variant: &str) -> Result<(), UiContextError> {
2308        self.set_theme_preference(
2309            ThemeTarget::Local,
2310            ThemePreference::Fixed {
2311                selection: ThemeSelection::new(family, variant),
2312            },
2313            format!("select subtree {family}/{variant}"),
2314        )
2315    }
2316
2317    /// Follow system appearance for one application theme family.
2318    ///
2319    /// # Errors
2320    ///
2321    /// Returns phase, theme, or borrow errors.
2322    pub fn set_theme_system(&self, family: &str) -> Result<(), UiContextError> {
2323        self.set_theme_preference(
2324            ThemeTarget::App,
2325            ThemePreference::System {
2326                family: family.to_owned(),
2327            },
2328            format!("follow system family {family}"),
2329        )
2330    }
2331
2332    /// Follow system appearance for the current window.
2333    ///
2334    /// # Errors
2335    ///
2336    /// Returns phase, missing-window, theme, or borrow errors.
2337    pub fn set_window_theme_system(&self, family: &str) -> Result<(), UiContextError> {
2338        let window = self.window.clone().ok_or(UiContextError::MissingWindow)?;
2339        self.set_theme_preference(
2340            ThemeTarget::Window(window),
2341            ThemePreference::System {
2342                family: family.to_owned(),
2343            },
2344            format!("follow window system family {family}"),
2345        )
2346    }
2347
2348    /// Follow system appearance for the current component subtree.
2349    ///
2350    /// # Errors
2351    ///
2352    /// Returns phase, theme, or borrow errors.
2353    pub fn set_local_theme_system(&self, family: &str) -> Result<(), UiContextError> {
2354        self.set_theme_preference(
2355            ThemeTarget::Local,
2356            ThemePreference::System {
2357                family: family.to_owned(),
2358            },
2359            format!("follow subtree system family {family}"),
2360        )
2361    }
2362
2363    fn set_theme_preference(
2364        &self,
2365        target: ThemeTarget,
2366        preference: ThemePreference,
2367        message: String,
2368    ) -> Result<(), UiContextError> {
2369        self.require_mutation()?;
2370        let mut runtime = self
2371            .runtime
2372            .try_borrow_mut()
2373            .map_err(|_| UiContextError::Borrowed)?;
2374        let theme = runtime
2375            .theme
2376            .as_mut()
2377            .ok_or(UiContextError::ThemeUnavailable)?;
2378        let app_target = matches!(&target, ThemeTarget::App);
2379        match target {
2380            ThemeTarget::App => theme.set_app(preference)?,
2381            ThemeTarget::Window(window) => theme.set_window(window, preference)?,
2382            ThemeTarget::Local => theme.set_scope(self.component.clone(), preference)?,
2383        }
2384        if app_target {
2385            runtime.mark_all_windows_dirty();
2386        } else {
2387            runtime.dirty.insert(self.component.clone());
2388        }
2389        runtime.traces.push(
2390            crate::RuntimeTraceKind::Theme,
2391            self.component.to_string(),
2392            message,
2393            None,
2394            false,
2395        );
2396        Ok(())
2397    }
2398
2399    /// Change the central motion preference and settle nonessential animation
2400    /// immediately when reduced motion is requested.
2401    ///
2402    /// # Errors
2403    ///
2404    /// Returns phase or runtime borrow errors.
2405    pub fn set_reduced_motion(&self, reduced: bool) -> Result<(), UiContextError> {
2406        self.require_mutation()?;
2407        let mut runtime = self
2408            .runtime
2409            .try_borrow_mut()
2410            .map_err(|_| UiContextError::Borrowed)?;
2411        runtime.animations.set_preference(if reduced {
2412            crate::MotionPreference::Reduced
2413        } else {
2414            crate::MotionPreference::Normal
2415        });
2416        let now = runtime.clock.now();
2417        runtime.animation_values = runtime.animations.snapshot(now);
2418        runtime.dirty.insert(self.component.clone());
2419        runtime.traces.push(
2420            crate::RuntimeTraceKind::State,
2421            self.component.to_string(),
2422            format!("reduced motion {reduced}"),
2423            None,
2424            false,
2425        );
2426        Ok(())
2427    }
2428
2429    /// Load and cache a logical image asset.
2430    ///
2431    /// # Errors
2432    ///
2433    /// Returns phase, provider, format, or borrow errors.
2434    pub fn load_image(&self, asset: &AssetId) -> Result<OpaqueHandle, UiContextError> {
2435        self.require_mutation()?;
2436        Ok(self
2437            .runtime
2438            .try_borrow()
2439            .map_err(|_| UiContextError::Borrowed)?
2440            .assets
2441            .load_image(asset)?
2442            .opaque()
2443            .clone())
2444    }
2445
2446    /// Start generation-bound background raster validation/decode.
2447    ///
2448    /// # Errors
2449    ///
2450    /// Returns phase, generation, provider, spawn, or borrow errors.
2451    pub fn start_image_decode(
2452        &self,
2453        asset: &AssetId,
2454        success: FnPtr,
2455        error: FnPtr,
2456    ) -> Result<ImageDecodeHandle, UiContextError> {
2457        self.require_mutation()?;
2458        self.require_generation()?;
2459        let success = self.scoped_callback(success)?;
2460        let error = self.scoped_callback(error)?;
2461        let mut runtime = self
2462            .runtime
2463            .try_borrow_mut()
2464            .map_err(|_| UiContextError::Borrowed)?;
2465        crate::RuntimeBudgets::check(
2466            "image_decodes",
2467            runtime.assets.pending_decode_count().saturating_add(1),
2468            runtime.budgets.image_decodes,
2469        )?;
2470        let handle = runtime.assets.start_image_decode(
2471            asset,
2472            self.async_scope
2473                .clone()
2474                .unwrap_or_else(|| AsyncScope::Component(self.component.clone())),
2475            self.generation,
2476            success,
2477            error,
2478        )?;
2479        runtime.traces.push(
2480            crate::RuntimeTraceKind::Task,
2481            self.component.to_string(),
2482            format!("decode image {}", asset.as_str()),
2483            None,
2484            false,
2485        );
2486        Ok(handle)
2487    }
2488
2489    /// Cancel a pending image decode owned by this runtime.
2490    ///
2491    /// # Errors
2492    ///
2493    /// Returns phase or borrow errors.
2494    pub fn cancel_image_decode(&self, handle: ImageDecodeHandle) -> Result<bool, UiContextError> {
2495        self.require_mutation()?;
2496        Ok(self
2497            .runtime
2498            .try_borrow()
2499            .map_err(|_| UiContextError::Borrowed)?
2500            .assets
2501            .cancel_image_decode(handle)?)
2502    }
2503
2504    /// Cancel a one-shot task owned by this runtime.
2505    ///
2506    /// # Errors
2507    ///
2508    /// Returns phase or borrow errors.
2509    pub fn cancel_task(&self, handle: TaskHandle) -> Result<bool, UiContextError> {
2510        self.require_mutation()?;
2511        Ok(self
2512            .runtime
2513            .try_borrow_mut()
2514            .map_err(|_| UiContextError::Borrowed)?
2515            .tasks
2516            .cancel(handle))
2517    }
2518
2519    /// Cancel a continuous subscription owned by this runtime.
2520    ///
2521    /// # Errors
2522    ///
2523    /// Returns phase or borrow errors.
2524    pub fn cancel_subscription(&self, handle: SubscriptionHandle) -> Result<bool, UiContextError> {
2525        self.require_mutation()?;
2526        Ok(self
2527            .runtime
2528            .try_borrow_mut()
2529            .map_err(|_| UiContextError::Borrowed)?
2530            .subscriptions
2531            .cancel(handle))
2532    }
2533
2534    /// Pause a declared timer by its component-local key.
2535    ///
2536    /// # Errors
2537    ///
2538    /// Returns phase, key, or borrow errors.
2539    pub fn pause_timeout(&self, key: &str) -> Result<bool, UiContextError> {
2540        self.require_mutation()?;
2541        let id = crate::TimerId::new(self.component.clone(), key)?;
2542        let mut runtime = self
2543            .runtime
2544            .try_borrow_mut()
2545            .map_err(|_| UiContextError::Borrowed)?;
2546        let now = runtime.clock.now();
2547        Ok(runtime.timers.pause(&id, now))
2548    }
2549
2550    /// Resume a declared timer by its component-local key.
2551    ///
2552    /// # Errors
2553    ///
2554    /// Returns phase, key, or borrow errors.
2555    pub fn resume_timeout(&self, key: &str) -> Result<bool, UiContextError> {
2556        self.require_mutation()?;
2557        let id = crate::TimerId::new(self.component.clone(), key)?;
2558        let mut runtime = self
2559            .runtime
2560            .try_borrow_mut()
2561            .map_err(|_| UiContextError::Borrowed)?;
2562        let now = runtime.clock.now();
2563        Ok(runtime.timers.resume(&id, now))
2564    }
2565
2566    /// Complete/cancel a declared timer until its signature changes or disappears.
2567    ///
2568    /// # Errors
2569    ///
2570    /// Returns phase, key, or borrow errors.
2571    pub fn cancel_timeout(&self, key: &str) -> Result<bool, UiContextError> {
2572        self.require_mutation()?;
2573        let id = crate::TimerId::new(self.component.clone(), key)?;
2574        Ok(self
2575            .runtime
2576            .try_borrow_mut()
2577            .map_err(|_| UiContextError::Borrowed)?
2578            .timers
2579            .cancel(&id))
2580    }
2581
2582    /// Start a one-shot asynchronous capability call.
2583    ///
2584    /// # Errors
2585    ///
2586    /// Returns phase, capability, schema, generation, spawn, or borrow errors.
2587    pub fn start_task(
2588        &self,
2589        capability: &str,
2590        method: &str,
2591        input: Dynamic,
2592        success: FnPtr,
2593        error: FnPtr,
2594    ) -> Result<TaskHandle, UiContextError> {
2595        self.require_mutation()?;
2596        self.require_async_start()?;
2597        self.require_generation()?;
2598        let id = CapabilityId::parse(capability)?;
2599        let input = UiValue::from_dynamic(input)?;
2600        let success = self.scoped_callback(success)?;
2601        let error = self.scoped_callback(error)?;
2602        let mut runtime = self
2603            .runtime
2604            .try_borrow_mut()
2605            .map_err(|_| UiContextError::Borrowed)?;
2606        crate::RuntimeBudgets::check(
2607            "background_tasks",
2608            runtime.tasks.active_count().saturating_add(1),
2609            runtime.budgets.background_tasks,
2610        )?;
2611        let (work, output) = runtime.capabilities.start_task(&id, method, input)?;
2612        let handle = runtime.tasks.spawn_cancellable(
2613            self.async_scope
2614                .clone()
2615                .unwrap_or_else(|| AsyncScope::Component(self.component.clone())),
2616            self.generation,
2617            success,
2618            error,
2619            output,
2620            move |cancellation| work.run(cancellation),
2621        )?;
2622        runtime.traces.push(
2623            crate::RuntimeTraceKind::Task,
2624            self.component.to_string(),
2625            format!("start {capability}.{method}"),
2626            None,
2627            true,
2628        );
2629        Ok(handle)
2630    }
2631
2632    /// Start a continuous asynchronous capability subscription.
2633    ///
2634    /// # Errors
2635    ///
2636    /// Returns phase, capability, schema, generation, spawn, or borrow errors.
2637    pub fn start_subscription(
2638        &self,
2639        capability: &str,
2640        method: &str,
2641        input: Dynamic,
2642        success: FnPtr,
2643        error: FnPtr,
2644        options: SubscriptionOptions,
2645    ) -> Result<SubscriptionHandle, UiContextError> {
2646        self.require_mutation()?;
2647        self.require_async_start()?;
2648        self.require_generation()?;
2649        if !matches!(self.async_scope, Some(AsyncScope::Effect { .. })) {
2650            return Err(UiContextError::SubscriptionRequiresEffect);
2651        }
2652        let id = CapabilityId::parse(capability)?;
2653        let input = UiValue::from_dynamic(input)?;
2654        let success = self.scoped_callback(success)?;
2655        let error = self.scoped_callback(error)?;
2656        let mut runtime = self
2657            .runtime
2658            .try_borrow_mut()
2659            .map_err(|_| UiContextError::Borrowed)?;
2660        crate::RuntimeBudgets::check(
2661            "subscriptions",
2662            runtime.subscriptions.active_count().saturating_add(1),
2663            runtime.budgets.subscriptions,
2664        )?;
2665        let (work, output) = runtime
2666            .capabilities
2667            .start_subscription(&id, method, input)?;
2668        let registration = SubscriptionRegistration::new(
2669            format!("{capability}.{method}"),
2670            self.async_scope
2671                .clone()
2672                .unwrap_or_else(|| AsyncScope::Component(self.component.clone())),
2673            self.generation,
2674            success,
2675            error,
2676            output,
2677        )
2678        .with_options(options);
2679        let (handle, emitter) = runtime.subscriptions.subscribe(registration);
2680        let closer = emitter.clone();
2681        if let Err(spawn_error) = std::thread::Builder::new()
2682            .name("gpui-rhai-subscription".to_owned())
2683            .spawn(move || run_subscription_work(work, emitter, &closer))
2684        {
2685            let _ = runtime
2686                .subscriptions
2687                .cancel_with_reason(handle, SubscriptionCloseReason::StartupFailed);
2688            return Err(AsyncRuntimeError::Spawn(spawn_error).into());
2689        }
2690        runtime.traces.push(
2691            crate::RuntimeTraceKind::Subscription,
2692            self.component.to_string(),
2693            format!("start {capability}.{method}"),
2694            None,
2695            true,
2696        );
2697        Ok(handle)
2698    }
2699
2700    fn require_mutation(&self) -> Result<(), UiContextError> {
2701        if self.phase.allows_mutation() {
2702            Ok(())
2703        } else {
2704            Err(UiContextError::MutationDuringRender)
2705        }
2706    }
2707
2708    fn require_async_start(&self) -> Result<(), UiContextError> {
2709        if self.phase.allows_async_start() {
2710            Ok(())
2711        } else {
2712            Err(UiContextError::AsyncDuringSuspend)
2713        }
2714    }
2715
2716    fn require_generation(&self) -> Result<(), UiContextError> {
2717        if self.generation == ScriptGeneration::default() {
2718            Err(UiContextError::MissingGeneration)
2719        } else {
2720            Ok(())
2721        }
2722    }
2723}
2724
2725fn run_subscription_work(
2726    work: crate::SubscriptionWork,
2727    emitter: crate::SubscriptionEmitter,
2728    closer: &crate::SubscriptionEmitter,
2729) {
2730    work.run(emitter);
2731    closer.close_with_reason(SubscriptionCloseReason::WorkReturned);
2732}
2733
2734impl CustomType for UiContext {
2735    fn build(mut builder: TypeBuilder<Self>) {
2736        builder
2737            .with_name("UiContext")
2738            .with_fn(
2739                "emit",
2740                |context: &mut Self, event: ImmutableString, payload: Dynamic| {
2741                    context
2742                        .emit(event.as_str(), payload)
2743                        .map_err(|error| Box::new(context_runtime_error(&error)))
2744                },
2745            )
2746            .with_fn(
2747                "component_style",
2748                |context: &mut Self, part: ImmutableString, base: crate::Style| {
2749                    context.resolve_component_style(part.as_str(), base)
2750                },
2751            )
2752            .with_fn(
2753                "call_capability",
2754                |context: &mut Self,
2755                 capability: ImmutableString,
2756                 method: ImmutableString,
2757                 input: Dynamic| {
2758                    context
2759                        .call_capability(capability.as_str(), method.as_str(), input)
2760                        .map(UiValue::into_dynamic)
2761                        .map_err(|error| Box::new(context_runtime_error(&error)))
2762                },
2763            );
2764        register_state_store_context_methods(&mut builder);
2765        register_native_collection_context_methods(&mut builder);
2766        register_native_document_context_methods(&mut builder);
2767        register_signal_context_methods(&mut builder);
2768        register_element_ref_context_methods(&mut builder);
2769        register_async_context_methods(&mut builder);
2770        register_action_context_methods(&mut builder);
2771        register_locale_context_methods(&mut builder);
2772        register_theme_context_methods(&mut builder);
2773        register_motion_context_methods(&mut builder);
2774        register_asset_context_methods(&mut builder);
2775        register_window_context_methods(&mut builder);
2776    }
2777}
2778
2779fn register_native_collection_context_methods(builder: &mut TypeBuilder<UiContext>) {
2780    builder.with_fn(
2781        "get_native_collection",
2782        |context: &mut UiContext, name: ImmutableString| {
2783            context
2784                .get_native_collection(name.as_str())
2785                .map_err(|error| Box::new(context_runtime_error(&error)))
2786        },
2787    );
2788}
2789
2790fn register_native_document_context_methods(builder: &mut TypeBuilder<UiContext>) {
2791    builder.with_fn(
2792        "get_native_text_document",
2793        |context: &mut UiContext, name: ImmutableString| {
2794            context
2795                .get_native_text_document(name.as_str())
2796                .map_err(|error| Box::new(context_runtime_error(&error)))
2797        },
2798    );
2799}
2800
2801fn register_state_store_context_methods(builder: &mut TypeBuilder<UiContext>) {
2802    register_state_context_methods(builder);
2803    register_app_store_context_methods(builder);
2804    register_window_store_context_methods(builder);
2805}
2806
2807fn register_state_context_methods(builder: &mut TypeBuilder<UiContext>) {
2808    builder
2809        .with_fn(
2810            "get_state",
2811            |context: &mut UiContext, field: ImmutableString| {
2812                context
2813                    .get_state(field.as_str())
2814                    .map(UiValue::into_dynamic)
2815                    .map_err(|error| Box::new(context_runtime_error(&error)))
2816            },
2817        )
2818        .with_fn(
2819            "get_state_path",
2820            |context: &mut UiContext, field: ImmutableString, path: Array| {
2821                let path = rhai_value_path(path)?;
2822                context
2823                    .get_state_path(field.as_str(), &path)
2824                    .map(UiValue::into_dynamic)
2825                    .map_err(|error| Box::new(context_runtime_error(&error)))
2826            },
2827        )
2828        .with_fn(
2829            "set_state",
2830            |context: &mut UiContext, field: ImmutableString, value: Dynamic| {
2831                context
2832                    .set_state(field.as_str(), value)
2833                    .map_err(|error| Box::new(context_runtime_error(&error)))
2834            },
2835        )
2836        .with_fn(
2837            "set_state_path",
2838            |context: &mut UiContext, field: ImmutableString, path: Array, value: Dynamic| {
2839                let path = rhai_value_path(path)?;
2840                context
2841                    .set_state_path(field.as_str(), &path, value)
2842                    .map_err(|error| Box::new(context_runtime_error(&error)))
2843            },
2844        );
2845}
2846
2847fn register_app_store_context_methods(builder: &mut TypeBuilder<UiContext>) {
2848    builder
2849        .with_fn(
2850            "get_app_store",
2851            |context: &mut UiContext, store: ImmutableString, field: ImmutableString| {
2852                context
2853                    .get_app_store(store.as_str(), field.as_str())
2854                    .map(UiValue::into_dynamic)
2855                    .map_err(|error| Box::new(context_runtime_error(&error)))
2856            },
2857        )
2858        .with_fn(
2859            "get_app_store_path",
2860            |context: &mut UiContext,
2861             store: ImmutableString,
2862             field: ImmutableString,
2863             path: Array| {
2864                let path = rhai_value_path(path)?;
2865                context
2866                    .get_app_store_path(store.as_str(), field.as_str(), &path)
2867                    .map(UiValue::into_dynamic)
2868                    .map_err(|error| Box::new(context_runtime_error(&error)))
2869            },
2870        )
2871        .with_fn(
2872            "set_app_store",
2873            |context: &mut UiContext,
2874             store: ImmutableString,
2875             field: ImmutableString,
2876             value: Dynamic| {
2877                context
2878                    .set_app_store(store.as_str(), field.as_str(), value)
2879                    .map_err(|error| Box::new(context_runtime_error(&error)))
2880            },
2881        )
2882        .with_fn(
2883            "set_app_store_path",
2884            |context: &mut UiContext,
2885             store: ImmutableString,
2886             field: ImmutableString,
2887             path: Array,
2888             value: Dynamic| {
2889                let path = rhai_value_path(path)?;
2890                context
2891                    .set_app_store_path(store.as_str(), field.as_str(), &path, value)
2892                    .map_err(|error| Box::new(context_runtime_error(&error)))
2893            },
2894        );
2895}
2896
2897fn register_window_store_context_methods(builder: &mut TypeBuilder<UiContext>) {
2898    builder
2899        .with_fn(
2900            "get_window_store",
2901            |context: &mut UiContext, store: ImmutableString, field: ImmutableString| {
2902                context
2903                    .get_window_store(store.as_str(), field.as_str())
2904                    .map(UiValue::into_dynamic)
2905                    .map_err(|error| Box::new(context_runtime_error(&error)))
2906            },
2907        )
2908        .with_fn(
2909            "get_window_store_path",
2910            |context: &mut UiContext,
2911             store: ImmutableString,
2912             field: ImmutableString,
2913             path: Array| {
2914                let path = rhai_value_path(path)?;
2915                context
2916                    .get_window_store_path(store.as_str(), field.as_str(), &path)
2917                    .map(UiValue::into_dynamic)
2918                    .map_err(|error| Box::new(context_runtime_error(&error)))
2919            },
2920        )
2921        .with_fn(
2922            "set_window_store",
2923            |context: &mut UiContext,
2924             store: ImmutableString,
2925             field: ImmutableString,
2926             value: Dynamic| {
2927                context
2928                    .set_window_store(store.as_str(), field.as_str(), value)
2929                    .map_err(|error| Box::new(context_runtime_error(&error)))
2930            },
2931        )
2932        .with_fn(
2933            "set_window_store_path",
2934            |context: &mut UiContext,
2935             store: ImmutableString,
2936             field: ImmutableString,
2937             path: Array,
2938             value: Dynamic| {
2939                let path = rhai_value_path(path)?;
2940                context
2941                    .set_window_store_path(store.as_str(), field.as_str(), &path, value)
2942                    .map_err(|error| Box::new(context_runtime_error(&error)))
2943            },
2944        );
2945}
2946
2947fn rhai_value_path(values: Array) -> Result<UiValuePath, Box<EvalAltResult>> {
2948    if values.len() > 64 {
2949        return Err(Box::new(EvalAltResult::ErrorRuntime(
2950            "value path must have at most 64 segments".into(),
2951            Position::NONE,
2952        )));
2953    }
2954    let segments = values
2955        .into_iter()
2956        .enumerate()
2957        .map(|(index, value)| {
2958            let actual = value.type_name().to_owned();
2959            if value.is::<ImmutableString>() {
2960                return Ok(UiValuePathSegment::Key(
2961                    value.cast::<ImmutableString>().to_string(),
2962                ));
2963            }
2964            if value.is::<INT>() {
2965                let index_value = value.cast::<INT>();
2966                return usize::try_from(index_value)
2967                    .map(UiValuePathSegment::Index)
2968                    .map_err(|_| {
2969                        Box::new(EvalAltResult::ErrorRuntime(
2970                            format!("value path segment {index} must be a non-negative integer")
2971                                .into(),
2972                            Position::NONE,
2973                        ))
2974                    });
2975            }
2976            if let Some(mut selector) = value.try_cast::<Map>() {
2977                let by = selector
2978                    .remove("by")
2979                    .and_then(Dynamic::try_cast::<ImmutableString>);
2980                let key = selector
2981                    .remove("key")
2982                    .and_then(Dynamic::try_cast::<ImmutableString>);
2983                if selector.is_empty()
2984                    && let (Some(by), Some(key)) = (by, key)
2985                {
2986                    return Ok(UiValuePathSegment::Item {
2987                        key_field: by.to_string(),
2988                        key: key.to_string(),
2989                    });
2990                }
2991                return Err(Box::new(EvalAltResult::ErrorRuntime(
2992                    format!(
2993                        "value path segment {index} keyed selector must be exactly #{{ by: string, key: string }}"
2994                    )
2995                    .into(),
2996                    Position::NONE,
2997                )));
2998            }
2999            Err(Box::new(EvalAltResult::ErrorRuntime(
3000                format!(
3001                    "value path segment {index} must be a string, non-negative integer, or keyed selector map; got {actual}"
3002                )
3003                .into(),
3004                Position::NONE,
3005            )))
3006        })
3007        .collect::<Result<Vec<_>, _>>()?;
3008    UiValuePath::new(segments).map_err(|error| {
3009        Box::new(EvalAltResult::ErrorRuntime(
3010            error.to_string().into(),
3011            Position::NONE,
3012        ))
3013    })
3014}
3015
3016fn register_signal_context_methods(builder: &mut TypeBuilder<UiContext>) {
3017    builder
3018        .with_fn(
3019            "get_signal",
3020            |context: &mut UiContext, signal: crate::NativeSignal| {
3021                context
3022                    .get_signal(&signal)
3023                    .map_err(|error| Box::new(context_runtime_error(&error)))
3024            },
3025        )
3026        .with_fn(
3027            "get_signal",
3028            |context: &mut UiContext, key: ImmutableString| {
3029                context
3030                    .get_signal_by_key(key.as_str())
3031                    .map_err(|error| Box::new(context_runtime_error(&error)))
3032            },
3033        )
3034        .with_fn(
3035            "set_signal",
3036            |context: &mut UiContext, signal: crate::NativeSignal, value: Dynamic| {
3037                context
3038                    .set_signal(&signal, value)
3039                    .map_err(|error| Box::new(context_runtime_error(&error)))
3040            },
3041        )
3042        .with_fn(
3043            "set_signal",
3044            |context: &mut UiContext, key: ImmutableString, value: Dynamic| {
3045                context
3046                    .set_signal_by_key(key.as_str(), value)
3047                    .map_err(|error| Box::new(context_runtime_error(&error)))
3048            },
3049        );
3050}
3051
3052fn register_element_ref_context_methods(builder: &mut TypeBuilder<UiContext>) {
3053    builder
3054        .with_fn(
3055            "element_bounds",
3056            |context: &mut UiContext, reference: crate::ElementRef| {
3057                context
3058                    .element_bounds(&reference)
3059                    .map(UiValue::into_dynamic)
3060                    .map_err(|error| Box::new(context_runtime_error(&error)))
3061            },
3062        )
3063        .with_fn(
3064            "element_bounds",
3065            |context: &mut UiContext, key: ImmutableString| {
3066                context
3067                    .element_bounds_by_key(key.as_str())
3068                    .map(UiValue::into_dynamic)
3069                    .map_err(|error| Box::new(context_runtime_error(&error)))
3070            },
3071        )
3072        .with_fn("event_target_bounds", |context: &mut UiContext| {
3073            context
3074                .event_target_bounds()
3075                .map(UiValue::into_dynamic)
3076                .map_err(|error| Box::new(context_runtime_error(&error)))
3077        })
3078        .with_fn(
3079            "focus",
3080            |context: &mut UiContext, reference: crate::ElementRef| {
3081                context
3082                    .focus_element(&reference)
3083                    .map_err(|error| Box::new(context_runtime_error(&error)))
3084            },
3085        )
3086        .with_fn("focus", |context: &mut UiContext, key: ImmutableString| {
3087            context
3088                .focus_element_by_key(key.as_str())
3089                .map_err(|error| Box::new(context_runtime_error(&error)))
3090        });
3091}
3092
3093fn geometry_bounds_value(bounds: crate::GeometryBounds) -> UiValue {
3094    bounds.into_value()
3095}
3096
3097fn register_action_context_methods(builder: &mut TypeBuilder<UiContext>) {
3098    builder
3099        .with_fn(
3100            "dispatch_action",
3101            |context: &mut UiContext, action: ImmutableString, payload: Dynamic| {
3102                context
3103                    .dispatch_action(action.as_str(), payload)
3104                    .map_err(|error| Box::new(context_runtime_error(&error)))
3105            },
3106        )
3107        .with_fn(
3108            "register_action",
3109            |call: NativeCallContext<'_>,
3110             context: &mut UiContext,
3111             action: ImmutableString,
3112             callback: FnPtr| {
3113                context_at_call(context, &call)
3114                    .register_action(action.as_str(), callback)
3115                    .map_err(|error| Box::new(context_runtime_error(&error)))
3116            },
3117        )
3118        .with_fn(
3119            "set_action_enabled",
3120            |context: &mut UiContext, action: ImmutableString, enabled: bool| {
3121                context
3122                    .set_action_enabled(action.as_str(), enabled)
3123                    .map_err(|error| Box::new(context_runtime_error(&error)))
3124            },
3125        )
3126        .with_fn(
3127            "scroll_to",
3128            |context: &mut UiContext, reference: crate::ElementRef, x: FLOAT, y: FLOAT| {
3129                context
3130                    .scroll_element_to(&reference, x, y)
3131                    .map_err(|error| Box::new(context_runtime_error(&error)))
3132            },
3133        )
3134        .with_fn(
3135            "scroll_to",
3136            |context: &mut UiContext, key: ImmutableString, x: FLOAT, y: FLOAT| {
3137                context
3138                    .scroll_element_to_by_key(key.as_str(), x, y)
3139                    .map_err(|error| Box::new(context_runtime_error(&error)))
3140            },
3141        )
3142        .with_fn(
3143            "scroll_into_view",
3144            |context: &mut UiContext, reference: crate::ElementRef| {
3145                context
3146                    .scroll_element_into_view(&reference)
3147                    .map_err(|error| Box::new(context_runtime_error(&error)))
3148            },
3149        )
3150        .with_fn(
3151            "scroll_into_view",
3152            |context: &mut UiContext, key: ImmutableString| {
3153                context
3154                    .scroll_element_into_view_by_key(key.as_str())
3155                    .map_err(|error| Box::new(context_runtime_error(&error)))
3156            },
3157        );
3158}
3159
3160fn register_async_context_methods(builder: &mut TypeBuilder<UiContext>) {
3161    builder
3162        .with_fn(
3163            "start_task",
3164            |call: NativeCallContext<'_>,
3165             context: &mut UiContext,
3166             capability: ImmutableString,
3167             method: ImmutableString,
3168             input: Dynamic,
3169             success: FnPtr,
3170             error: FnPtr| {
3171                context_at_call(context, &call)
3172                    .start_task(capability.as_str(), method.as_str(), input, success, error)
3173                    .map_err(|error| Box::new(context_runtime_error(&error)))
3174            },
3175        )
3176        .with_fn(
3177            "start_subscription",
3178            |call: NativeCallContext<'_>,
3179             context: &mut UiContext,
3180             capability: ImmutableString,
3181             method: ImmutableString,
3182             input: Dynamic,
3183             success: FnPtr,
3184             error: FnPtr,
3185             options: Map| {
3186                let options = subscription_options(options)?;
3187                context_at_call(context, &call)
3188                    .start_subscription(
3189                        capability.as_str(),
3190                        method.as_str(),
3191                        input,
3192                        success,
3193                        error,
3194                        options,
3195                    )
3196                    .map_err(|error| Box::new(context_runtime_error(&error)))
3197            },
3198        )
3199        .with_fn(
3200            "cancel_task",
3201            |context: &mut UiContext, handle: TaskHandle| {
3202                context
3203                    .cancel_task(handle)
3204                    .map_err(|error| Box::new(context_runtime_error(&error)))
3205            },
3206        )
3207        .with_fn(
3208            "cancel_subscription",
3209            |context: &mut UiContext, handle: SubscriptionHandle| {
3210                context
3211                    .cancel_subscription(handle)
3212                    .map_err(|error| Box::new(context_runtime_error(&error)))
3213            },
3214        )
3215        .with_fn(
3216            "pause_timeout",
3217            |context: &mut UiContext, key: ImmutableString| {
3218                context
3219                    .pause_timeout(key.as_str())
3220                    .map_err(|error| Box::new(context_runtime_error(&error)))
3221            },
3222        )
3223        .with_fn(
3224            "resume_timeout",
3225            |context: &mut UiContext, key: ImmutableString| {
3226                context
3227                    .resume_timeout(key.as_str())
3228                    .map_err(|error| Box::new(context_runtime_error(&error)))
3229            },
3230        )
3231        .with_fn(
3232            "cancel_timeout",
3233            |context: &mut UiContext, key: ImmutableString| {
3234                context
3235                    .cancel_timeout(key.as_str())
3236                    .map_err(|error| Box::new(context_runtime_error(&error)))
3237            },
3238        );
3239}
3240
3241fn subscription_options(mut options: Map) -> Result<SubscriptionOptions, Box<EvalAltResult>> {
3242    let defaults = SubscriptionOptions::default();
3243    let delivery = options.remove("delivery").map_or(
3244        Ok(defaults.delivery()),
3245        |value| -> Result<_, Box<EvalAltResult>> {
3246            let value = value.try_cast::<ImmutableString>().ok_or_else(|| {
3247                Box::new(EvalAltResult::ErrorRuntime(
3248                    "subscription delivery must be `all` or `latest`".into(),
3249                    Position::NONE,
3250                ))
3251            })?;
3252            match value.as_str() {
3253                "all" => Ok(SubscriptionDeliveryPolicy::All),
3254                "latest" => Ok(SubscriptionDeliveryPolicy::Latest),
3255                _ => Err(Box::new(EvalAltResult::ErrorRuntime(
3256                    "subscription delivery must be `all` or `latest`".into(),
3257                    Position::NONE,
3258                ))),
3259            }
3260        },
3261    )?;
3262    let capacity = options.remove("capacity").map_or(
3263        Ok(defaults.capacity()),
3264        |value| -> Result<_, Box<EvalAltResult>> {
3265            value
3266                .try_cast::<INT>()
3267                .and_then(|value| usize::try_from(value).ok())
3268                .ok_or_else(|| {
3269                    Box::new(EvalAltResult::ErrorRuntime(
3270                        "subscription capacity must be a positive integer".into(),
3271                        Position::NONE,
3272                    ))
3273                })
3274        },
3275    )?;
3276    let throttle = options.remove("throttle_ms").map_or(
3277        Ok(defaults.throttle()),
3278        |value| -> Result<_, Box<EvalAltResult>> {
3279            value
3280                .try_cast::<INT>()
3281                .and_then(|value| u64::try_from(value).ok())
3282                .map(Duration::from_millis)
3283                .ok_or_else(|| {
3284                    Box::new(EvalAltResult::ErrorRuntime(
3285                        "subscription throttle_ms must be a non-negative integer".into(),
3286                        Position::NONE,
3287                    ))
3288                })
3289        },
3290    )?;
3291    if let Some(name) = options.keys().next() {
3292        return Err(Box::new(EvalAltResult::ErrorRuntime(
3293            format!("unknown subscription option `{name}`").into(),
3294            Position::NONE,
3295        )));
3296    }
3297    SubscriptionOptions::new(delivery, capacity, throttle).map_err(|error| {
3298        Box::new(EvalAltResult::ErrorRuntime(
3299            error.to_string().into(),
3300            Position::NONE,
3301        ))
3302    })
3303}
3304
3305fn register_locale_context_methods(builder: &mut TypeBuilder<UiContext>) {
3306    builder
3307        .with_fn("t", |context: &mut UiContext, key: ImmutableString| {
3308            context
3309                .text(key.as_str())
3310                .map_err(|error| Box::new(context_runtime_error(&error)))
3311        })
3312        .with_fn(
3313            "set_locale",
3314            |context: &mut UiContext, locale: ImmutableString| {
3315                context
3316                    .set_locale(locale.as_str())
3317                    .map_err(|error| Box::new(context_runtime_error(&error)))
3318            },
3319        )
3320        .with_fn("text_direction", |context: &mut UiContext| {
3321            context
3322                .text_direction()
3323                .map_err(|error| Box::new(context_runtime_error(&error)))
3324        })
3325        .with_fn("today", |context: &mut UiContext| {
3326            context
3327                .today()
3328                .map_err(|error| Box::new(context_runtime_error(&error)))
3329        })
3330        .with_fn(
3331            "format_date",
3332            |context: &mut UiContext, date: ImmutableString, style: ImmutableString| {
3333                context
3334                    .format_date(date.as_str(), style.as_str())
3335                    .map_err(|error| Box::new(context_runtime_error(&error)))
3336            },
3337        )
3338        .with_fn(
3339            "format_month_year",
3340            |context: &mut UiContext, date: ImmutableString| {
3341                context
3342                    .format_month_year(date.as_str())
3343                    .map_err(|error| Box::new(context_runtime_error(&error)))
3344            },
3345        )
3346        .with_fn("calendar", |context: &mut UiContext| {
3347            context
3348                .calendar_metadata()
3349                .map_err(|error| Box::new(context_runtime_error(&error)))
3350        })
3351        .with_fn("number", |context: &mut UiContext| {
3352            context
3353                .number_metadata()
3354                .map_err(|error| Box::new(context_runtime_error(&error)))
3355        })
3356        .with_fn("format_number", |context: &mut UiContext, value: INT| {
3357            context
3358                .format_integer(value, NumberFormatOptions::default())
3359                .map_err(|error| Box::new(context_runtime_error(&error)))
3360        })
3361        .with_fn("format_number", |context: &mut UiContext, value: FLOAT| {
3362            context
3363                .format_number(value, NumberFormatOptions::default())
3364                .map_err(|error| Box::new(context_runtime_error(&error)))
3365        })
3366        .with_fn(
3367            "format_number",
3368            |context: &mut UiContext,
3369             value: INT,
3370             options: Map|
3371             -> Result<String, Box<EvalAltResult>> {
3372                let options = number_format_options(options)?;
3373                context
3374                    .format_integer(value, options)
3375                    .map_err(|error| Box::new(context_runtime_error(&error)))
3376            },
3377        )
3378        .with_fn(
3379            "format_number",
3380            |context: &mut UiContext,
3381             value: FLOAT,
3382             options: Map|
3383             -> Result<String, Box<EvalAltResult>> {
3384                let options = number_format_options(options)?;
3385                context
3386                    .format_number(value, options)
3387                    .map_err(|error| Box::new(context_runtime_error(&error)))
3388            },
3389        );
3390}
3391
3392fn number_format_options(mut options: Map) -> Result<NumberFormatOptions, Box<EvalAltResult>> {
3393    let mut parsed = NumberFormatOptions::default();
3394    if let Some(value) = options.remove("min_fraction_digits") {
3395        let value = value.try_cast::<INT>().ok_or_else(|| {
3396            Box::new(EvalAltResult::ErrorRuntime(
3397                "min_fraction_digits must be an integer".into(),
3398                Position::NONE,
3399            ))
3400        })?;
3401        parsed.min_fraction_digits = u8::try_from(value).map_err(|_| {
3402            Box::new(EvalAltResult::ErrorRuntime(
3403                "min_fraction_digits must be between 0 and 12".into(),
3404                Position::NONE,
3405            ))
3406        })?;
3407    }
3408    if let Some(value) = options.remove("max_fraction_digits") {
3409        let value = value.try_cast::<INT>().ok_or_else(|| {
3410            Box::new(EvalAltResult::ErrorRuntime(
3411                "max_fraction_digits must be an integer".into(),
3412                Position::NONE,
3413            ))
3414        })?;
3415        parsed.max_fraction_digits = u8::try_from(value).map_err(|_| {
3416            Box::new(EvalAltResult::ErrorRuntime(
3417                "max_fraction_digits must be between 0 and 12".into(),
3418                Position::NONE,
3419            ))
3420        })?;
3421    }
3422    if let Some(value) = options.remove("grouping") {
3423        parsed.grouping = value.try_cast::<bool>().ok_or_else(|| {
3424            Box::new(EvalAltResult::ErrorRuntime(
3425                "grouping must be a bool".into(),
3426                Position::NONE,
3427            ))
3428        })?;
3429    }
3430    if let Some((unknown, _)) = options.into_iter().next() {
3431        return Err(Box::new(EvalAltResult::ErrorRuntime(
3432            format!("unknown number format option `{unknown}`").into(),
3433            Position::NONE,
3434        )));
3435    }
3436    parsed.validate().map_err(|error| {
3437        Box::new(EvalAltResult::ErrorRuntime(
3438            error.to_string().into(),
3439            Position::NONE,
3440        ))
3441    })?;
3442    Ok(parsed)
3443}
3444
3445fn register_theme_context_methods(builder: &mut TypeBuilder<UiContext>) {
3446    builder
3447        .with_fn(
3448            "set_theme",
3449            |context: &mut UiContext, family: ImmutableString, variant: ImmutableString| {
3450                context
3451                    .set_theme(family.as_str(), variant.as_str())
3452                    .map_err(|error| Box::new(context_runtime_error(&error)))
3453            },
3454        )
3455        .with_fn(
3456            "set_window_theme",
3457            |context: &mut UiContext, family: ImmutableString, variant: ImmutableString| {
3458                context
3459                    .set_window_theme(family.as_str(), variant.as_str())
3460                    .map_err(|error| Box::new(context_runtime_error(&error)))
3461            },
3462        )
3463        .with_fn(
3464            "set_local_theme",
3465            |context: &mut UiContext, family: ImmutableString, variant: ImmutableString| {
3466                context
3467                    .set_local_theme(family.as_str(), variant.as_str())
3468                    .map_err(|error| Box::new(context_runtime_error(&error)))
3469            },
3470        )
3471        .with_fn(
3472            "set_theme_system",
3473            |context: &mut UiContext, family: ImmutableString| {
3474                context
3475                    .set_theme_system(family.as_str())
3476                    .map_err(|error| Box::new(context_runtime_error(&error)))
3477            },
3478        )
3479        .with_fn(
3480            "set_window_theme_system",
3481            |context: &mut UiContext, family: ImmutableString| {
3482                context
3483                    .set_window_theme_system(family.as_str())
3484                    .map_err(|error| Box::new(context_runtime_error(&error)))
3485            },
3486        )
3487        .with_fn(
3488            "set_local_theme_system",
3489            |context: &mut UiContext, family: ImmutableString| {
3490                context
3491                    .set_local_theme_system(family.as_str())
3492                    .map_err(|error| Box::new(context_runtime_error(&error)))
3493            },
3494        );
3495}
3496
3497fn register_motion_context_methods(builder: &mut TypeBuilder<UiContext>) {
3498    builder.with_fn(
3499        "set_reduced_motion",
3500        |context: &mut UiContext, reduced: bool| {
3501            context
3502                .set_reduced_motion(reduced)
3503                .map_err(|error| Box::new(context_runtime_error(&error)))
3504        },
3505    );
3506}
3507
3508fn register_asset_context_methods(builder: &mut TypeBuilder<UiContext>) {
3509    builder
3510        .with_fn("load_image", |context: &mut UiContext, asset: AssetId| {
3511            context
3512                .load_image(&asset)
3513                .map_err(|error| Box::new(context_runtime_error(&error)))
3514        })
3515        .with_fn(
3516            "start_image_decode",
3517            |call: NativeCallContext<'_>,
3518             context: &mut UiContext,
3519             asset: AssetId,
3520             success: FnPtr,
3521             error: FnPtr| {
3522                context_at_call(context, &call)
3523                    .start_image_decode(&asset, success, error)
3524                    .map_err(|error| Box::new(context_runtime_error(&error)))
3525            },
3526        )
3527        .with_fn(
3528            "cancel_image_decode",
3529            |context: &mut UiContext, handle: ImageDecodeHandle| {
3530                context
3531                    .cancel_image_decode(handle)
3532                    .map_err(|error| Box::new(context_runtime_error(&error)))
3533            },
3534        );
3535}
3536
3537fn register_window_context_methods(builder: &mut TypeBuilder<UiContext>) {
3538    builder
3539        .with_fn("window_id", |context: &mut UiContext| {
3540            context
3541                .window_id()
3542                .map_err(|error| Box::new(context_runtime_error(&error)))
3543        })
3544        .with_fn("view_id", |context: &mut UiContext| {
3545            context
3546                .view_id()
3547                .map_err(|error| Box::new(context_runtime_error(&error)))
3548        })
3549        .with_fn("viewport_class", |context: &mut UiContext| {
3550            context
3551                .viewport_class()
3552                .map_err(|error| Box::new(context_runtime_error(&error)))
3553        })
3554        .with_fn(
3555            "open_window",
3556            |context: &mut UiContext,
3557             id: ImmutableString,
3558             title: ImmutableString,
3559             width: rhai::INT,
3560             height: rhai::INT,
3561             focus: bool| {
3562                context
3563                    .open_window(ScriptWindowSpec {
3564                        id: id.to_string(),
3565                        title: title.to_string(),
3566                        width: width.to_string().parse::<f64>().unwrap_or(f64::INFINITY),
3567                        height: height.to_string().parse::<f64>().unwrap_or(f64::INFINITY),
3568                        focus,
3569                    })
3570                    .map_err(|error| Box::new(context_runtime_error(&error)))
3571            },
3572        )
3573        .with_fn(
3574            "focus_window",
3575            |context: &mut UiContext, id: ImmutableString| {
3576                context
3577                    .focus_window(id.as_str())
3578                    .map_err(|error| Box::new(context_runtime_error(&error)))
3579            },
3580        )
3581        .with_fn(
3582            "close_window",
3583            |context: &mut UiContext, id: ImmutableString| {
3584                context
3585                    .close_window(id.as_str())
3586                    .map_err(|error| Box::new(context_runtime_error(&error)))
3587            },
3588        )
3589        .with_fn(
3590            "set_close_handler",
3591            |call: NativeCallContext<'_>, context: &mut UiContext, callback: FnPtr| {
3592                context_at_call(context, &call)
3593                    .set_close_handler(callback)
3594                    .map_err(|error| Box::new(context_runtime_error(&error)))
3595            },
3596        )
3597        .with_fn("clear_close_handler", |context: &mut UiContext| {
3598            context
3599                .clear_close_handler()
3600                .map_err(|error| Box::new(context_runtime_error(&error)))
3601        })
3602        .with_fn(
3603            "set_window_locale",
3604            |context: &mut UiContext, locale: ImmutableString| {
3605                context
3606                    .set_window_locale(locale.as_str())
3607                    .map_err(|error| Box::new(context_runtime_error(&error)))
3608            },
3609        )
3610        .with_fn(
3611            "set_local_locale",
3612            |context: &mut UiContext, locale: ImmutableString| {
3613                context
3614                    .set_local_locale(locale.as_str())
3615                    .map_err(|error| Box::new(context_runtime_error(&error)))
3616            },
3617        );
3618}
3619
3620fn context_at_call(context: &UiContext, call: &NativeCallContext<'_>) -> UiContext {
3621    context
3622        .clone()
3623        .with_native_context(Some(crate::invocation::ScriptInvocationContext::capture(
3624            call,
3625        )))
3626}
3627
3628pub(crate) fn register_ui_context_api(engine: &mut Engine) {
3629    engine.build_type::<UiContext>();
3630}
3631
3632fn context_runtime_error(error: &UiContextError) -> EvalAltResult {
3633    EvalAltResult::ErrorRuntime(error.to_string().into(), Position::NONE)
3634}
3635
3636#[derive(Debug, Error)]
3637pub enum UiContextError {
3638    #[error("UI runtime state is already mutably borrowed by another callback")]
3639    Borrowed,
3640    #[error("state mutation and effects are forbidden during view rendering")]
3641    MutationDuringRender,
3642    #[error("new async work is forbidden while a view is suspending")]
3643    AsyncDuringSuspend,
3644    #[error("subscriptions must be started by a declarative component effect")]
3645    SubscriptionRequiresEffect,
3646    #[error("event target geometry is available only during event callbacks")]
3647    EventTargetOutsideEvent,
3648    #[error("async work requires a bound script generation")]
3649    MissingGeneration,
3650    #[error("no locale manager is configured for this application")]
3651    LocaleUnavailable,
3652    #[error("no theme manager is configured for this application")]
3653    ThemeUnavailable,
3654    #[error("component `{component}` has no state field `{field}`")]
3655    UnknownState {
3656        component: ComponentInstancePath,
3657        field: String,
3658    },
3659    #[error("event `{0}` is not declared by this component")]
3660    UnknownEvent(String),
3661    #[error("event payload is invalid: {0}")]
3662    InvalidEvent(crate::SchemaValidationError),
3663    #[error("this UI context is not associated with a window")]
3664    MissingWindow,
3665    #[error("this UI context is not associated with a mounted script view")]
3666    MissingView,
3667    #[error(transparent)]
3668    Window(#[from] WindowCommandError),
3669    #[error(transparent)]
3670    Responsive(#[from] ResponsiveError),
3671    #[error(transparent)]
3672    State(#[from] StateError),
3673    #[error(transparent)]
3674    Store(#[from] StoreError),
3675    #[error(transparent)]
3676    NativeCollection(#[from] crate::NativeCollectionError),
3677    #[error(transparent)]
3678    Document(#[from] crate::DocumentError),
3679    #[error(transparent)]
3680    Action(#[from] ActionError),
3681    #[error(transparent)]
3682    Capability(#[from] CapabilityError),
3683    #[error(transparent)]
3684    Async(#[from] AsyncRuntimeError),
3685    #[error(transparent)]
3686    Locale(#[from] LocaleError),
3687    #[error(transparent)]
3688    Theme(#[from] ThemeError),
3689    #[error(transparent)]
3690    Asset(#[from] AssetError),
3691    #[error(transparent)]
3692    Value(#[from] UiValueError),
3693    #[error(transparent)]
3694    ValuePath(#[from] UiValuePathError),
3695    #[error(transparent)]
3696    Signal(#[from] crate::SignalError),
3697    #[error(transparent)]
3698    ElementRef(#[from] crate::ElementRefError),
3699    #[error(transparent)]
3700    Geometry(#[from] crate::GeometryError),
3701    #[error("scroll offset must be finite and non-negative, got ({x}, {y})")]
3702    InvalidScrollOffset { x: f64, y: f64 },
3703    #[error(transparent)]
3704    Callback(#[from] crate::ScriptCallbackDefinitionError),
3705    #[error(transparent)]
3706    Timer(#[from] crate::TimerError),
3707    #[error(transparent)]
3708    Budget(#[from] crate::RuntimeBudgetError),
3709}
3710
3711#[cfg(test)]
3712mod tests {
3713    use super::*;
3714    use crate::{ComponentStateSchema, RuntimeEngine, StateField, SubscriptionWork, ValueSchema};
3715
3716    fn mounted_context(phase: ExecutionPhase) -> UiContext {
3717        let path = ComponentInstancePath::root("Counter", "counter");
3718        let schema = ComponentStateSchema::new(BTreeMap::from([
3719            (
3720                "count".to_owned(),
3721                StateField::new(ValueSchema::integer(), UiValue::Integer(0)),
3722            ),
3723            (
3724                "profile".to_owned(),
3725                StateField::new(
3726                    ValueSchema::UiValue,
3727                    UiValue::Map(BTreeMap::from([(
3728                        "name".to_owned(),
3729                        UiValue::String("Ada".to_owned()),
3730                    )])),
3731                ),
3732            ),
3733        ]))
3734        .unwrap();
3735        let mut state = UiRuntimeState::new();
3736        let mut render = state.component_state.begin_render();
3737        render.mount(path.clone(), &schema).unwrap();
3738        state.component_state.commit_render(render);
3739        UiContext::new(
3740            Rc::new(RefCell::new(state)),
3741            path,
3742            Some("main".to_owned()),
3743            phase,
3744            BTreeMap::from([(
3745                "change".to_owned(),
3746                EventSchema {
3747                    payload: ValueSchema::integer(),
3748                },
3749            )]),
3750        )
3751    }
3752
3753    #[test]
3754    fn render_signal_reads_disable_component_reuse() {
3755        let context = mounted_context(ExecutionPhase::Render);
3756        let signal = crate::NativeSignal::new(
3757            crate::SignalId::new(
3758                context.component_path().clone(),
3759                "hot",
3760                crate::SignalKind::Float,
3761            )
3762            .unwrap(),
3763        );
3764        assert!(context.get_signal(&signal).is_err());
3765        assert!(!context.component_render_is_reusable());
3766    }
3767
3768    #[test]
3769    fn render_context_rejects_mutation() {
3770        let context = mounted_context(ExecutionPhase::Render);
3771        assert!(matches!(
3772            context.set_state("count", Dynamic::from(1_i64)),
3773            Err(UiContextError::MutationDuringRender)
3774        ));
3775    }
3776
3777    #[test]
3778    fn event_target_bounds_are_untracked_and_event_only() {
3779        let context = mounted_context(ExecutionPhase::Event).with_event_target(Some(
3780            crate::GeometryBounds::new(13.0, 24.0, 90.0, 40.0).unwrap(),
3781        ));
3782        assert_eq!(
3783            context.event_target_bounds().unwrap(),
3784            crate::GeometryBounds::new(13.0, 24.0, 90.0, 40.0)
3785                .unwrap()
3786                .into_value()
3787        );
3788        assert!(context.runtime().borrow().geometry.take_dirty().is_empty());
3789
3790        assert!(matches!(
3791            mounted_context(ExecutionPhase::Render).event_target_bounds(),
3792            Err(UiContextError::EventTargetOutsideEvent)
3793        ));
3794        assert_eq!(
3795            mounted_context(ExecutionPhase::Event)
3796                .event_target_bounds()
3797                .unwrap(),
3798            UiValue::Null
3799        );
3800    }
3801
3802    #[test]
3803    fn viewport_class_is_window_scoped_and_script_readable() {
3804        let context = mounted_context(ExecutionPhase::Render);
3805        context
3806            .runtime()
3807            .borrow_mut()
3808            .responsive
3809            .update_window("main", 480.0)
3810            .unwrap();
3811        assert_eq!(context.viewport_class().unwrap(), "compact");
3812    }
3813
3814    #[test]
3815    fn locale_and_viewport_reads_register_exact_component_dependencies() {
3816        let engine = Engine::new();
3817        let en = crate::load_locale_source(
3818            &engine,
3819            "en.rhai",
3820            include_str!("../../../registry/locales/en.rhai"),
3821        )
3822        .unwrap();
3823        let zh = crate::load_locale_source(
3824            &engine,
3825            "zh_cn.rhai",
3826            include_str!("../../../registry/locales/zh_cn.rhai"),
3827        )
3828        .unwrap();
3829        let mut state = UiRuntimeState::new();
3830        state.locale = Some(LocaleManager::new([en, zh], "en", "en").unwrap());
3831        state.responsive.update_window("main", 480.0).unwrap();
3832        state.windows.register_open("main").unwrap();
3833        let runtime = Rc::new(RefCell::new(state));
3834        let reader = ComponentInstancePath::root("View", "main").child("Reader", "reader");
3835        let unrelated = ComponentInstancePath::root("View", "main").child("Static", "static");
3836        let render_context = UiContext::new(
3837            Rc::clone(&runtime),
3838            reader.clone(),
3839            Some("main".to_owned()),
3840            ExecutionPhase::Render,
3841            BTreeMap::new(),
3842        );
3843        assert!(!render_context.text("common.loading").unwrap().is_empty());
3844        assert_eq!(render_context.viewport_class().unwrap(), "compact");
3845        let event = UiContext::new(
3846            Rc::clone(&runtime),
3847            unrelated,
3848            Some("main".to_owned()),
3849            ExecutionPhase::Event,
3850            BTreeMap::new(),
3851        );
3852        event.set_locale("zh-CN").unwrap();
3853
3854        assert_eq!(
3855            runtime.borrow_mut().drain_batch().dirty,
3856            BTreeSet::from([reader.clone()])
3857        );
3858        assert_eq!(
3859            runtime
3860                .borrow()
3861                .environment_dependencies
3862                .invalidate_viewport("main"),
3863            BTreeSet::from([reader])
3864        );
3865        assert!(runtime.borrow_mut().take_window_repaint("main"));
3866    }
3867
3868    #[test]
3869    fn event_mutations_are_batched() {
3870        let context = mounted_context(ExecutionPhase::Event);
3871        context.set_state("count", Dynamic::from(1_i64)).unwrap();
3872        context.set_state("count", Dynamic::from(2_i64)).unwrap();
3873        context.emit("change", Dynamic::from(2_i64)).unwrap();
3874
3875        let mut runtime = context.runtime().borrow_mut();
3876        let batch = runtime.drain_batch();
3877        assert_eq!(batch.dirty.len(), 1);
3878        assert_eq!(batch.events.len(), 1);
3879        assert_eq!(
3880            runtime
3881                .component_state
3882                .get(&ComponentInstancePath::root("Counter", "counter"), "count",),
3883            Some(&UiValue::Integer(2))
3884        );
3885    }
3886
3887    #[test]
3888    fn semantic_event_and_action_traces_never_retain_payload_values() {
3889        let secret = "AUDIT_ONLY_SYNTHETIC_TOKEN";
3890        let path = ComponentInstancePath::root("SecretInput", "audit");
3891        let schema = ComponentStateSchema::new(BTreeMap::from([(
3892            "token".to_owned(),
3893            StateField::new(ValueSchema::string(), UiValue::String(secret.to_owned()))
3894                .sensitive(true),
3895        )]))
3896        .unwrap();
3897        let mut state = UiRuntimeState::new();
3898        let mut render = state.component_state.begin_render();
3899        render.mount(path.clone(), &schema).unwrap();
3900        state.component_state.commit_render(render);
3901
3902        let mut engine = RuntimeEngine::new();
3903        let compiled = engine
3904            .compile("fn view() { text(\"trace\") } fn receive(ctx, payload) {}")
3905            .unwrap();
3906        state.actions.register_or_replace(
3907            crate::ActionId::parse("audit.receive").unwrap(),
3908            engine.callback(&compiled, "receive").unwrap(),
3909        );
3910        let runtime = Rc::new(RefCell::new(state));
3911        let context = UiContext::new(
3912            Rc::clone(&runtime),
3913            path,
3914            Some("main".to_owned()),
3915            ExecutionPhase::Event,
3916            BTreeMap::from([(
3917                "change".to_owned(),
3918                EventSchema {
3919                    payload: ValueSchema::string(),
3920                },
3921            )]),
3922        );
3923
3924        let event_value = context.get_state("token").unwrap().into_dynamic();
3925        context.emit("change", event_value).unwrap();
3926        let action_value = context.get_state("token").unwrap().into_dynamic();
3927        context
3928            .dispatch_action("audit.receive", action_value)
3929            .unwrap();
3930
3931        let traces = runtime.borrow().traces.snapshot();
3932        let semantic = traces
3933            .iter()
3934            .filter(|trace| {
3935                matches!(
3936                    trace.kind,
3937                    crate::RuntimeTraceKind::Event | crate::RuntimeTraceKind::Action
3938                )
3939            })
3940            .collect::<Vec<_>>();
3941        assert_eq!(semantic.len(), 2);
3942        assert!(
3943            semantic
3944                .iter()
3945                .all(|trace| trace.sensitive && trace.payload.is_none())
3946        );
3947        assert!(!format!("{traces:?}").contains(secret));
3948    }
3949
3950    #[test]
3951    fn scripts_receive_restricted_context_without_gpui_types() {
3952        let context = mounted_context(ExecutionPhase::Event);
3953        let mut runtime = RuntimeEngine::new();
3954        let compiled = runtime
3955            .compile(
3956                r#"
3957                    fn view() { text("context") }
3958                    fn increment(ctx) {
3959                        ctx.set_state("count", ctx.get_state("count") + 1);
3960                    }
3961                "#,
3962            )
3963            .unwrap();
3964        runtime.render(&compiled).unwrap();
3965        let callback = runtime.callback(&compiled, "increment").unwrap();
3966        let _ = runtime
3967            .invoke_callback(&compiled, &callback, (context.clone(),))
3968            .unwrap();
3969        assert_eq!(context.get_state("count").unwrap(), UiValue::Integer(1));
3970    }
3971
3972    #[test]
3973    fn scripts_use_bounded_nested_and_keyed_store_paths() {
3974        let context = mounted_context(ExecutionPhase::Event);
3975        let row = |id: &str, label: &str| {
3976            UiValue::Map(BTreeMap::from([
3977                ("id".to_owned(), UiValue::String(id.to_owned())),
3978                ("label".to_owned(), UiValue::String(label.to_owned())),
3979            ]))
3980        };
3981        context
3982            .runtime()
3983            .borrow_mut()
3984            .stores
3985            .declare(
3986                StoreId::app("model"),
3987                ComponentStateSchema::new(BTreeMap::from([(
3988                    "data".to_owned(),
3989                    StateField::new(
3990                        ValueSchema::UiValue,
3991                        UiValue::Map(BTreeMap::from([(
3992                            "rows".to_owned(),
3993                            UiValue::Array(vec![row("alpha", "Alpha"), row("beta", "Beta")]),
3994                        )])),
3995                    ),
3996                )]))
3997                .unwrap(),
3998            )
3999            .unwrap();
4000        let mut runtime = RuntimeEngine::new();
4001        let compiled = runtime
4002            .compile(
4003                r#"
4004                    fn view() { text("paths") }
4005                    fn update(ctx) {
4006                        let name = ctx.get_state_path("profile", ["name"]);
4007                        ctx.set_state_path("profile", ["name"], name + " Lovelace");
4008                        let label = ctx.get_app_store_path(
4009                            "model",
4010                            "data",
4011                            ["rows", #{ by: "id", key: "beta" }, "label"]
4012                        );
4013                        ctx.set_app_store_path(
4014                            "model",
4015                            "data",
4016                            ["rows", #{ by: "id", key: "beta" }, "label"],
4017                            label + "!"
4018                        );
4019                    }
4020                "#,
4021            )
4022            .unwrap();
4023        runtime.render(&compiled).unwrap();
4024        let callback = runtime.callback(&compiled, "update").unwrap();
4025        let _: Dynamic = runtime
4026            .invoke_callback(&compiled, &callback, (context.clone(),))
4027            .unwrap();
4028
4029        let profile = context.get_state("profile").unwrap();
4030        assert_eq!(
4031            profile
4032                .get_path(
4033                    &UiValuePath::new(vec![UiValuePathSegment::Key("name".to_owned())]).unwrap()
4034                )
4035                .unwrap(),
4036            &UiValue::String("Ada Lovelace".to_owned())
4037        );
4038        let row_path = UiValuePath::new(vec![
4039            UiValuePathSegment::Key("rows".to_owned()),
4040            UiValuePathSegment::Item {
4041                key_field: "id".to_owned(),
4042                key: "beta".to_owned(),
4043            },
4044            UiValuePathSegment::Key("label".to_owned()),
4045        ])
4046        .unwrap();
4047        assert_eq!(
4048            context
4049                .runtime()
4050                .borrow_mut()
4051                .stores
4052                .read_path_tracked(
4053                    &ComponentInstancePath::root("Test", "reader"),
4054                    &StoreId::app("model"),
4055                    "data",
4056                    &row_path,
4057                )
4058                .unwrap(),
4059            UiValue::String("Beta!".to_owned())
4060        );
4061        assert!(rhai_value_path(vec![Dynamic::from(-1_i64)]).is_err());
4062    }
4063
4064    #[test]
4065    fn script_theme_switch_preserves_component_state_and_ast_generation() {
4066        let context = mounted_context(ExecutionPhase::Event);
4067        context.set_state("count", Dynamic::from(7_i64)).unwrap();
4068        let mut runtime = RuntimeEngine::new();
4069        let dark = crate::load_theme_source(
4070            runtime.engine(),
4071            "default_dark.rhai",
4072            include_str!("../../../registry/themes/default_dark.rhai"),
4073        )
4074        .unwrap();
4075        let mocha = crate::load_theme_source(
4076            runtime.engine(),
4077            "catppuccin_mocha.rhai",
4078            include_str!("../../../registry/themes/catppuccin_mocha.rhai"),
4079        )
4080        .unwrap();
4081        context.runtime().borrow_mut().theme = Some(
4082            ThemeManager::from_variants([dark, mocha], ThemeSelection::new("Default", "Dark"))
4083                .unwrap(),
4084        );
4085        let compiled = runtime
4086            .compile(
4087                r#"
4088                    fn view() { text("theme") }
4089                    fn switch_theme(ctx) { ctx.set_theme("Catppuccin", "Mocha"); }
4090                "#,
4091            )
4092            .unwrap();
4093        runtime.render(&compiled).unwrap();
4094        let generation = compiled.generation();
4095        let callback = runtime.callback(&compiled, "switch_theme").unwrap();
4096        let _ = runtime
4097            .invoke_callback(&compiled, &callback, (context.clone(),))
4098            .unwrap();
4099
4100        assert_eq!(context.get_state("count").unwrap(), UiValue::Integer(7));
4101        assert_eq!(compiled.generation(), generation);
4102        let state = context.runtime().borrow();
4103        let theme = state
4104            .theme
4105            .as_ref()
4106            .unwrap()
4107            .resolve(Some("main"), None, crate::SystemAppearance::Dark)
4108            .unwrap();
4109        assert_eq!(theme.variant().family, "Catppuccin");
4110        assert_eq!(theme.variant().name, "Mocha");
4111        drop(state);
4112        context.set_theme_system("Default").unwrap();
4113        let state = context.runtime().borrow();
4114        assert!(matches!(
4115            state.theme.as_ref().unwrap().app_preference(),
4116            ThemePreference::System { family } if family == "Default"
4117        ));
4118    }
4119
4120    #[test]
4121    fn script_reads_fixed_today_calendar_and_locale_formatters() {
4122        let mut engine = Engine::new();
4123        register_ui_context_api(&mut engine);
4124        let bundle = crate::load_locale_source(
4125            &engine,
4126            "en.rhai",
4127            include_str!("../../../registry/locales/en.rhai"),
4128        )
4129        .unwrap();
4130        let mut state = UiRuntimeState::new();
4131        state.locale = Some(LocaleManager::new([bundle], "en", "en").unwrap());
4132        state.calendar_clock =
4133            crate::CalendarClock::fixed(crate::GregorianDate::parse_iso("2026-08-29").unwrap());
4134        let context = UiContext::new(
4135            Rc::new(RefCell::new(state)),
4136            ComponentInstancePath::root("App", "root"),
4137            Some("main".to_owned()),
4138            ExecutionPhase::Render,
4139            BTreeMap::new(),
4140        );
4141        let mut scope = rhai::Scope::new();
4142        scope.push("ctx", context);
4143        let values = engine
4144            .eval_with_scope::<rhai::Array>(
4145                &mut scope,
4146                r#"[
4147                    ctx.today(),
4148                    ctx.format_date("2024-02-29", "long"),
4149                    ctx.format_number(12345, #{ min_fraction_digits: 0, max_fraction_digits: 0 }),
4150                    ctx.calendar().first_weekday,
4151                ]"#,
4152            )
4153            .unwrap();
4154        assert_eq!(values[0].clone_cast::<String>(), "2026-08-29");
4155        assert_eq!(
4156            values[1].clone_cast::<String>(),
4157            "Thursday, February 29, 2024"
4158        );
4159        assert_eq!(values[2].clone_cast::<String>(), "12,345");
4160        assert_eq!(values[3].clone_cast::<String>(), "sunday");
4161    }
4162
4163    #[test]
4164    fn returning_subscription_work_closes_external_emitters_with_reason() {
4165        let mut engine = RuntimeEngine::new();
4166        let compiled = engine
4167            .compile(
4168                r#"
4169                    fn view() { text("subscription") }
4170                    fn success(ctx, value) { value }
4171                    fn failure(ctx, error) { error }
4172                "#,
4173            )
4174            .unwrap();
4175        let generation = compiled.generation();
4176        let success = engine.callback(&compiled, "success").unwrap();
4177        let failure = engine.callback(&compiled, "failure").unwrap();
4178        let mut state = UiRuntimeState::new();
4179        let registration = SubscriptionRegistration::new(
4180            "app.stream.watch",
4181            AsyncScope::App,
4182            generation,
4183            success,
4184            failure,
4185            ValueSchema::integer(),
4186        );
4187        let (_, emitter) = state.subscriptions.subscribe(registration);
4188        let external = emitter.clone();
4189        run_subscription_work(SubscriptionWork::new(|_| {}), emitter.clone(), &emitter);
4190
4191        assert!(matches!(
4192            external.emit(UiValue::Integer(1)),
4193            Err(AsyncRuntimeError::Closed {
4194                reason: SubscriptionCloseReason::WorkReturned
4195            })
4196        ));
4197        let _ = state.subscriptions.drain(generation);
4198        assert_eq!(
4199            state.subscriptions.take_closures()[0].reason,
4200            SubscriptionCloseReason::WorkReturned
4201        );
4202        let registration = SubscriptionRegistration::new(
4203            "app.stream.trace",
4204            AsyncScope::App,
4205            generation,
4206            engine.callback(&compiled, "success").unwrap(),
4207            engine.callback(&compiled, "failure").unwrap(),
4208            ValueSchema::integer(),
4209        );
4210        let (_, emitter) = state.subscriptions.subscribe(registration);
4211        emitter.close_with_reason(SubscriptionCloseReason::WorkReturned);
4212        let _ = state.subscriptions.drain(generation);
4213        state.trace_subscription_closures();
4214        let traces = state.traces.snapshot();
4215        assert!(traces.iter().any(|trace| {
4216            trace.kind == crate::RuntimeTraceKind::Subscription
4217                && trace.message == "close app.stream.trace: the subscription work returned"
4218        }));
4219    }
4220
4221    #[test]
4222    fn receiver_subscription_forwards_external_values_until_sender_drop() {
4223        let mut engine = RuntimeEngine::new();
4224        let compiled = engine
4225            .compile(
4226                r#"
4227                    fn view() { text("subscription") }
4228                    fn success(ctx, value) { value }
4229                    fn failure(ctx, error) { error }
4230                "#,
4231            )
4232            .unwrap();
4233        let generation = compiled.generation();
4234        let registration = SubscriptionRegistration::new(
4235            "app.stream.receiver",
4236            AsyncScope::App,
4237            generation,
4238            engine.callback(&compiled, "success").unwrap(),
4239            engine.callback(&compiled, "failure").unwrap(),
4240            ValueSchema::integer(),
4241        );
4242        let mut subscriptions = SubscriptionRegistry::new();
4243        let (_, emitter) = subscriptions.subscribe(registration);
4244        let external = emitter.clone();
4245        let closer = emitter.clone();
4246        let (sender, receiver) = std::sync::mpsc::channel();
4247        let worker = std::thread::spawn(move || {
4248            run_subscription_work(SubscriptionWork::from_receiver(receiver), emitter, &closer);
4249        });
4250        sender.send(UiValue::Integer(42)).unwrap();
4251        drop(sender);
4252        worker.join().unwrap();
4253
4254        let deliveries = subscriptions.drain(generation);
4255        assert_eq!(deliveries[0].payload, UiValue::Integer(42));
4256        assert!(matches!(
4257            external.emit(UiValue::Integer(43)),
4258            Err(AsyncRuntimeError::Closed {
4259                reason: SubscriptionCloseReason::WorkReturned
4260            })
4261        ));
4262    }
4263
4264    #[test]
4265    fn receiver_subscription_waits_for_lossless_capacity_without_ending_the_stream() {
4266        let mut engine = RuntimeEngine::new();
4267        let compiled = engine
4268            .compile(
4269                "fn view() { text(\"stream\") } fn success(ctx, value) {} fn failure(ctx, value) {}",
4270            )
4271            .unwrap();
4272        engine.render(&compiled).unwrap();
4273        let generation = compiled.generation();
4274        let registration = SubscriptionRegistration::new(
4275            "stream",
4276            AsyncScope::App,
4277            generation,
4278            engine.callback(&compiled, "success").unwrap(),
4279            engine.callback(&compiled, "failure").unwrap(),
4280            ValueSchema::integer(),
4281        )
4282        .with_capacity(1)
4283        .unwrap();
4284        let mut subscriptions = SubscriptionRegistry::new();
4285        let (_, emitter) = subscriptions.subscribe(registration);
4286        let closer = emitter.clone();
4287        let (sender, receiver) = std::sync::mpsc::channel();
4288        let worker = std::thread::spawn(move || {
4289            run_subscription_work(SubscriptionWork::from_receiver(receiver), emitter, &closer);
4290        });
4291        sender.send(UiValue::Integer(1)).unwrap();
4292        sender.send(UiValue::Integer(2)).unwrap();
4293        let deadline = std::time::Instant::now() + Duration::from_secs(1);
4294        let first = loop {
4295            let values = subscriptions.drain(generation);
4296            if !values.is_empty() {
4297                break values;
4298            }
4299            assert!(std::time::Instant::now() < deadline);
4300            std::thread::yield_now();
4301        };
4302        assert_eq!(first[0].payload, UiValue::Integer(1));
4303        let second = loop {
4304            let values = subscriptions.drain(generation);
4305            if !values.is_empty() {
4306                break values;
4307            }
4308            assert!(std::time::Instant::now() < deadline);
4309            std::thread::yield_now();
4310        };
4311        assert_eq!(second[0].payload, UiValue::Integer(2));
4312        assert_eq!(subscriptions.active_count(), 1);
4313        drop(sender);
4314        worker.join().unwrap();
4315    }
4316
4317    #[test]
4318    fn runtime_transaction_rollback_restores_cancelled_subscription() {
4319        let mut engine = RuntimeEngine::new();
4320        let compiled = engine
4321            .compile(
4322                "fn view() { text(\"test\") } fn success(ctx, value) {} fn failure(ctx, value) {}",
4323            )
4324            .unwrap();
4325        engine.render(&compiled).unwrap();
4326        let success = engine.callback(&compiled, "success").unwrap();
4327        let error = engine.callback(&compiled, "failure").unwrap();
4328        let generation = compiled.generation();
4329        let mut state = UiRuntimeState::new();
4330        let (handle, emitter) = state.subscriptions.subscribe(SubscriptionRegistration::new(
4331            "rollback",
4332            AsyncScope::App,
4333            generation,
4334            success,
4335            error,
4336            ValueSchema::integer(),
4337        ));
4338        let checkpoint = state.begin_transaction().unwrap();
4339        assert!(state.subscriptions.cancel(handle));
4340        assert_eq!(emitter.close_reason(), None);
4341        state.rollback_transaction(checkpoint).unwrap();
4342        assert_eq!(state.subscriptions.active_count(), 1);
4343        emitter.emit(UiValue::Integer(7)).unwrap();
4344        assert_eq!(state.subscriptions.drain(generation).len(), 1);
4345    }
4346
4347    #[test]
4348    fn transaction_tokens_cannot_cross_runtime_or_commit_unbalanced() {
4349        let mut first = UiRuntimeState::new();
4350        let mut second = UiRuntimeState::new();
4351        let checkpoint = first.begin_transaction().unwrap();
4352        assert!(matches!(
4353            second.rollback_transaction(checkpoint.clone()),
4354            Err(UiTransactionError::ForeignSnapshot)
4355        ));
4356        assert!(matches!(
4357            second.commit_transaction(),
4358            Err(UiTransactionError::Unbalanced { .. })
4359        ));
4360        first.rollback_transaction(checkpoint).unwrap();
4361    }
4362
4363    #[test]
4364    fn presentation_geometry_and_capture_are_isolated_per_view() {
4365        let mut first = crate::RetainedUiTree::new();
4366        first.reconcile(crate::UiNode::text("first")).unwrap();
4367        let mut second = crate::RetainedUiTree::new();
4368        second.reconcile(crate::UiNode::text("second")).unwrap();
4369        let first_node = first.root_id().unwrap();
4370        let second_node = second.root_id().unwrap();
4371        assert_eq!(
4372            first_node, second_node,
4373            "tree-local IDs intentionally overlap"
4374        );
4375
4376        let mut state = UiRuntimeState::new();
4377        state.ensure_presentation("first");
4378        state.ensure_presentation("second");
4379        let first_geometry = state.geometry_for(Some("first"));
4380        let second_geometry = state.geometry_for(Some("second"));
4381        let bounds = |width| crate::ElementGeometry {
4382            layout: crate::GeometryBounds::new(0.0, 0.0, width, 20.0).unwrap(),
4383            visual: crate::GeometryBounds::new(0.0, 0.0, width, 20.0).unwrap(),
4384            clip: None,
4385        };
4386        first_geometry.update(first_node, bounds(100.0));
4387        second_geometry.update(second_node, bounds(300.0));
4388        second_geometry.begin_frame();
4389        assert!(
4390            (first_geometry.get(first_node).unwrap().visual.width - 100.0).abs() < f64::EPSILON
4391        );
4392        assert!(first_geometry.is_presented(first_node));
4393
4394        let first_capture = state.pointer_capture_for(Some("first"));
4395        let second_capture = state.pointer_capture_for(Some("second"));
4396        first_capture.capture(0, first_node);
4397        second_capture.capture(0, second_node);
4398        assert_eq!(first_capture.captured(0), Some(first_node));
4399        assert_eq!(second_capture.captured(0), Some(second_node));
4400    }
4401
4402    #[test]
4403    fn focus_by_ref_key_queues_a_window_scoped_retained_command() {
4404        let context = mounted_context(ExecutionPhase::Event);
4405        let mut tree = crate::RetainedUiTree::new();
4406        tree.reconcile(crate::UiNode::text("field")).unwrap();
4407        let reference = crate::ElementRef::new(
4408            crate::ElementRefId::new(context.component_path().clone(), "field").unwrap(),
4409        );
4410        context.runtime().borrow_mut().element_refs.reconcile(
4411            context.component_path(),
4412            BTreeMap::from([(reference.id().clone(), tree.root_id().unwrap())]),
4413        );
4414        let node = tree.root_id().unwrap();
4415        context
4416            .runtime()
4417            .borrow()
4418            .geometry_for(Some("main"))
4419            .update(
4420                node,
4421                crate::ElementGeometry {
4422                    layout: crate::GeometryBounds::new(1.0, 2.0, 120.0, 24.0).unwrap(),
4423                    visual: crate::GeometryBounds::new(3.0, 4.0, 120.0, 24.0).unwrap(),
4424                    clip: None,
4425                },
4426            );
4427        assert!(matches!(
4428            context.element_bounds_by_key("field").unwrap(),
4429            UiValue::Map(bounds)
4430                if matches!(bounds.get("visual"), Some(UiValue::Map(visual))
4431                    if visual.get("width") == Some(&UiValue::Float(120.0)))
4432        ));
4433        context.focus_element_by_key("field").unwrap();
4434        context
4435            .scroll_element_to_by_key("field", 12.0, 24.0)
4436            .unwrap();
4437        context.scroll_element_into_view_by_key("field").unwrap();
4438        let commands = context
4439            .runtime()
4440            .borrow_mut()
4441            .take_window_element_commands("main");
4442        assert!(matches!(
4443            commands.as_slice(),
4444            [crate::element_ref::ElementCommand::Focus { node, .. },
4445             crate::element_ref::ElementCommand::ScrollTo { node: scroll_node, x, y, .. },
4446             crate::element_ref::ElementCommand::ScrollIntoView { node: reveal_node, .. }]
4447                if *node == tree.root_id().unwrap()
4448                    && scroll_node == node
4449                    && reveal_node == node
4450                    && (*x - 12.0).abs() < f64::EPSILON
4451                    && (*y - 24.0).abs() < f64::EPSILON
4452        ));
4453    }
4454}