Skip to main content

gpui_rhai/
context.rs

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