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