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