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