Skip to main content

gpui_rhai/
context.rs

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