Skip to main content

gpui_rhai/
app.rs

1use std::borrow::Cow;
2use std::cell::{Cell, RefCell};
3use std::collections::{BTreeMap, BTreeSet};
4use std::fs;
5use std::path::{Path, PathBuf};
6use std::rc::Rc;
7use std::time::{Duration, Instant};
8
9use gpui::KeyBinding;
10use gpui::actions;
11use gpui::{
12    AnyElement, AnyWindowHandle, App, AppContext, Bounds, Context, DispatchPhase, Element,
13    ElementId, Entity, FocusHandle, Global, GlobalElementId, InspectorElementId,
14    InteractiveElement, IntoElement, LayoutId, MouseButton, MouseDownEvent, ParentElement, Pixels,
15    Render, ScrollAnchor, ScrollHandle, SharedString, Styled, Subscription, Task, TitlebarOptions,
16    Window, WindowAppearance, WindowBounds, WindowOptions, deferred, div, px, rgba, size,
17};
18use thiserror::Error;
19
20#[cfg(feature = "dev-reload")]
21use crate::FileWatcher;
22use crate::overlay_element::WindowOverlayCoordinator;
23use crate::{
24    ActionError, ActionId, AppManifest, AssetData, AssetId, AssetRegistry, CapabilityError,
25    CompiledUi, ComponentExportError, ComponentInstancePath, ComponentRegistry,
26    ComponentStateSchema, ComponentStyleError, ComponentStyleSheet, DependencyError, Diagnostic,
27    DirectoryAssetProvider, DispatchScriptAction, EmbeddedScriptSource, FileScriptSource,
28    GpuiNodeRenderer, InMemoryAssetProvider, InteractionState, KeyBindingSpec, LocaleBundle,
29    LocaleManager, ModuleCompileCache, ModuleId, MotionPreference, MotionRuntime,
30    NodeEventDispatcher, PrimitiveRegistry, ResponsiveError, ResponsiveRuntime,
31    RestrictedModuleResolver, RuntimeEngine, RuntimeError, ScriptCallback, ScriptLifecycle,
32    ScriptSource, ScriptWindowSpec, SystemAppearance, TextDirection, ThemeManager, ThemeSelection,
33    ThemeSnapshot, ThemeTokenOverrides, ThemeVariant, UiRuntimeState, UiValue, ViewportBreakpoints,
34    WindowCommand, WindowCommandPolicy, init_text_area, init_text_input, load_component_styles,
35    load_locale_source,
36};
37
38#[cfg(feature = "dev-reload")]
39actions!(gpui_rhai_devtools, [ToggleInspector]);
40actions!(gpui_rhai_host, [CopySelectedText]);
41
42const HOST_KEY_CONTEXT: &str = "GPUIRhaiHost";
43
44#[derive(Default)]
45struct ScriptRuntimeInstallation {
46    bindings: BTreeMap<(String, Option<String>), ActionId>,
47    loaded_fonts: BTreeSet<u64>,
48}
49
50impl Global for ScriptRuntimeInstallation {}
51
52/// Install GPUI Rhai's application-wide input actions once.
53pub fn install(cx: &mut App) {
54    if cx.has_global::<ScriptRuntimeInstallation>() {
55        return;
56    }
57    init_text_input(cx);
58    init_text_area(cx);
59    crate::init_document_view(cx);
60    cx.bind_keys([KeyBinding::new(
61        "cmd-c",
62        CopySelectedText,
63        Some(HOST_KEY_CONTEXT),
64    )]);
65    cx.set_global(ScriptRuntimeInstallation::default());
66}
67
68#[derive(Clone, Debug)]
69pub struct ScriptViewConfig {
70    view_id: String,
71    paint_background: bool,
72    show_error_banner: bool,
73}
74
75impl ScriptViewConfig {
76    #[must_use]
77    pub fn new(view_id: impl Into<String>) -> Self {
78        Self {
79            view_id: view_id.into(),
80            paint_background: false,
81            show_error_banner: true,
82        }
83    }
84
85    #[must_use]
86    pub const fn paint_background(mut self, paint: bool) -> Self {
87        self.paint_background = paint;
88        self
89    }
90
91    /// Control the built-in selectable runtime-error banner for this view.
92    ///
93    /// Hosts that disable it must surface [`ScriptViewHandle::last_error`]
94    /// themselves so failed candidates are not silent.
95    #[must_use]
96    pub const fn show_error_banner(mut self, show: bool) -> Self {
97        self.show_error_banner = show;
98        self
99    }
100
101    #[must_use]
102    pub fn view_id(&self) -> &str {
103        &self.view_id
104    }
105}
106
107#[derive(Clone)]
108pub struct ScriptViewHost {
109    inner: Rc<RefCell<ScriptViewHostState>>,
110}
111
112struct ScriptViewHostState {
113    window_id: String,
114    overlays: WindowOverlayCoordinator,
115    fallback_focus: FocusHandle,
116    views: BTreeMap<String, FocusHandle>,
117    pending_focus_recovery: Vec<FocusHandle>,
118    overlay_viewport: Option<crate::OverlayBounds>,
119    window_policy: WindowCommandPolicy,
120    frame_active: bool,
121    container_bounds: Option<Bounds<Pixels>>,
122}
123
124impl ScriptViewHost {
125    /// Create one interaction/overlay domain, normally one per GPUI window.
126    ///
127    /// # Errors
128    ///
129    /// Returns [`ScriptViewError::InvalidId`] for an unsafe window identifier.
130    pub fn new(window_id: impl Into<String>, cx: &mut App) -> Result<Self, ScriptViewError> {
131        Self::new_with_policy(window_id, WindowCommandPolicy::Disabled, cx)
132    }
133
134    fn new_with_policy(
135        window_id: impl Into<String>,
136        window_policy: WindowCommandPolicy,
137        cx: &mut App,
138    ) -> Result<Self, ScriptViewError> {
139        install(cx);
140        let window_id = window_id.into();
141        validate_view_id(&window_id)?;
142        Ok(Self {
143            inner: Rc::new(RefCell::new(ScriptViewHostState {
144                window_id,
145                overlays: WindowOverlayCoordinator::default(),
146                fallback_focus: cx.focus_handle(),
147                views: BTreeMap::new(),
148                pending_focus_recovery: Vec::new(),
149                overlay_viewport: None,
150                window_policy,
151                frame_active: false,
152                container_bounds: None,
153            })),
154        })
155    }
156
157    #[must_use]
158    pub fn window_id(&self) -> String {
159        self.inner.borrow().window_id.clone()
160    }
161
162    /// Restrict this Host's overlays to an explicit absolute rectangle.
163    ///
164    /// # Errors
165    ///
166    /// Returns [`ScriptViewError::Overlay`] for invalid geometry.
167    pub fn set_overlay_viewport(
168        &self,
169        viewport: crate::OverlayBounds,
170    ) -> Result<(), ScriptViewError> {
171        crate::OverlayManager::new(viewport)?;
172        self.inner.borrow_mut().overlay_viewport = Some(viewport);
173        Ok(())
174    }
175
176    pub fn use_window_overlay_viewport(&self) {
177        self.inner.borrow_mut().overlay_viewport = None;
178    }
179
180    #[must_use]
181    pub fn overlay_placement(
182        &self,
183        view_id: &str,
184        local_id: &str,
185    ) -> Option<crate::PlacementResult> {
186        self.inner
187            .borrow()
188            .overlays
189            .placement(view_id, &crate::OverlayId::new(local_id))
190    }
191
192    /// Bind host-approved script actions once at App scope.
193    ///
194    /// # Errors
195    ///
196    /// Returns a conflict when another binding already owns the same keys and
197    /// context, or an invalid-key error from GPUI conversion.
198    pub fn bind_keys(
199        &self,
200        bindings: impl IntoIterator<Item = KeyBindingSpec>,
201        cx: &mut App,
202    ) -> Result<(), ScriptViewError> {
203        install(cx);
204        let bindings = bindings.into_iter().collect::<Vec<_>>();
205        let converted = bindings
206            .iter()
207            .map(KeyBindingSpec::to_gpui)
208            .collect::<Result<Vec<_>, _>>()?;
209        let mut new = Vec::new();
210        {
211            let installed = cx.global_mut::<ScriptRuntimeInstallation>();
212            for (index, binding) in bindings.iter().enumerate() {
213                let key = (binding.keystrokes.clone(), binding.context.clone());
214                if let Some(existing) = installed.bindings.get(&key)
215                    && existing != &binding.action
216                {
217                    return Err(ScriptViewError::KeyBindingConflict {
218                        keystrokes: binding.keystrokes.clone(),
219                        context: binding.context.clone(),
220                        existing: existing.clone(),
221                        requested: binding.action.clone(),
222                    });
223                }
224                if let std::collections::btree_map::Entry::Vacant(entry) =
225                    installed.bindings.entry(key)
226                {
227                    entry.insert(binding.action.clone());
228                    new.push(index);
229                }
230            }
231        }
232        cx.bind_keys(new.into_iter().map(|index| converted[index].clone()));
233        Ok(())
234    }
235
236    #[must_use]
237    pub fn container(&self, child: impl IntoElement) -> AnyElement {
238        let (fallback, overlays) = {
239            let state = self.inner.borrow();
240            (state.fallback_focus.clone(), state.overlays.clone())
241        };
242        let escape_overlays = overlays.clone();
243        let child = div()
244            .size_full()
245            .track_focus(&fallback)
246            .on_key_down(move |event, window, cx| {
247                if event.keystroke.key.as_str() == "escape"
248                    && escape_overlays.dismiss_escape(window, cx)
249                {
250                    cx.stop_propagation();
251                }
252            })
253            .child(child)
254            .child(SharedLayerPortalElement {
255                coordinator: overlays,
256            })
257            .into_any_element();
258        ScriptViewHostFrame {
259            host: self.clone(),
260            child: Some(child),
261        }
262        .into_any_element()
263    }
264
265    fn reserve_view(&self, view_id: &str) -> Result<(), ScriptViewError> {
266        validate_view_id(view_id)?;
267        let mut state = self.inner.borrow_mut();
268        if state.views.contains_key(view_id) {
269            return Err(ScriptViewError::DuplicateView(view_id.to_owned()));
270        }
271        let fallback = state.fallback_focus.clone();
272        state.views.insert(view_id.to_owned(), fallback);
273        Ok(())
274    }
275
276    fn attach_view_focus(&self, view_id: &str, focus: FocusHandle) {
277        if let Some(entry) = self.inner.borrow_mut().views.get_mut(view_id) {
278            *entry = focus;
279        }
280    }
281
282    fn unregister_view(&self, view_id: &str) {
283        let mut state = self.inner.borrow_mut();
284        if let Some(focus) = state.views.remove(view_id) {
285            state.pending_focus_recovery.push(focus);
286        }
287        state.overlays.remove_view(view_id);
288    }
289
290    fn quiesce_view(&self, view_id: &str, focus: &FocusHandle, window: &mut Window, cx: &mut App) {
291        let fallback = self.inner.borrow().fallback_focus.clone();
292        if focus.contains_focused(window, cx) {
293            fallback.focus(window, cx);
294        }
295        self.inner.borrow().overlays.remove_view(view_id);
296    }
297
298    fn overlays(&self) -> WindowOverlayCoordinator {
299        self.inner.borrow().overlays.clone()
300    }
301
302    fn window_policy(&self) -> WindowCommandPolicy {
303        self.inner.borrow().window_policy
304    }
305
306    fn frame_active(&self) -> bool {
307        self.inner.borrow().frame_active
308    }
309}
310
311fn validate_view_id(id: &str) -> Result<(), ScriptViewError> {
312    let valid = (1..=64).contains(&id.len())
313        && id
314            .chars()
315            .next()
316            .is_some_and(|character| character.is_ascii_alphanumeric())
317        && id
318            .chars()
319            .all(|character| character.is_ascii_alphanumeric() || matches!(character, '_' | '-'));
320    if valid {
321        Ok(())
322    } else {
323        Err(ScriptViewError::InvalidId(id.to_owned()))
324    }
325}
326
327struct ScriptViewHostFrame {
328    host: ScriptViewHost,
329    child: Option<AnyElement>,
330}
331
332impl Element for ScriptViewHostFrame {
333    type RequestLayoutState = AnyElement;
334    type PrepaintState = ();
335
336    fn id(&self) -> Option<ElementId> {
337        None
338    }
339
340    fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
341        None
342    }
343
344    fn request_layout(
345        &mut self,
346        _id: Option<&GlobalElementId>,
347        _inspector_id: Option<&InspectorElementId>,
348        window: &mut Window,
349        cx: &mut App,
350    ) -> (LayoutId, Self::RequestLayoutState) {
351        let (overlays, viewport, fallback, pending) = {
352            let mut state = self.host.inner.borrow_mut();
353            state.frame_active = true;
354            let viewport = state
355                .overlay_viewport
356                .unwrap_or_else(|| crate::OverlayBounds {
357                    x: 0.0,
358                    y: 0.0,
359                    width: f64::from(window.viewport_size().width),
360                    height: f64::from(window.viewport_size().height),
361                });
362            (
363                state.overlays.clone(),
364                viewport,
365                state.fallback_focus.clone(),
366                std::mem::take(&mut state.pending_focus_recovery),
367            )
368        };
369        overlays.begin_host_frame(viewport);
370        if pending
371            .iter()
372            .any(|focus| focus.contains_focused(window, cx))
373        {
374            fallback.focus(window, cx);
375        }
376        let mut child = self.child.take().expect("host frame lays out once");
377        let layout = child.request_layout(window, cx);
378        (layout, child)
379    }
380
381    fn prepaint(
382        &mut self,
383        _id: Option<&GlobalElementId>,
384        _inspector_id: Option<&InspectorElementId>,
385        bounds: Bounds<Pixels>,
386        child: &mut Self::RequestLayoutState,
387        window: &mut Window,
388        cx: &mut App,
389    ) {
390        self.host.inner.borrow_mut().container_bounds = Some(bounds);
391        child.prepaint(window, cx);
392    }
393
394    fn paint(
395        &mut self,
396        _id: Option<&GlobalElementId>,
397        _inspector_id: Option<&InspectorElementId>,
398        _bounds: Bounds<Pixels>,
399        child: &mut Self::RequestLayoutState,
400        _prepaint: &mut Self::PrepaintState,
401        window: &mut Window,
402        cx: &mut App,
403    ) {
404        child.paint(window, cx);
405        self.host.inner.borrow_mut().frame_active = false;
406        let overlays = self.host.overlays();
407        let container_bounds = self.host.inner.borrow().container_bounds;
408        window.on_mouse_event(move |event: &MouseDownEvent, phase, window, cx| {
409            if phase == DispatchPhase::Capture
410                && container_bounds.is_some_and(|bounds| bounds.contains(&event.position))
411            {
412                let _ = overlays.dismiss_outside(event.position, window, cx);
413            }
414        });
415    }
416}
417
418impl IntoElement for ScriptViewHostFrame {
419    type Element = Self;
420
421    fn into_element(self) -> Self::Element {
422        self
423    }
424}
425
426struct SharedLayerPortalElement {
427    coordinator: WindowOverlayCoordinator,
428}
429
430impl Element for SharedLayerPortalElement {
431    type RequestLayoutState = AnyElement;
432    type PrepaintState = ();
433
434    fn id(&self) -> Option<ElementId> {
435        None
436    }
437
438    fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
439        None
440    }
441
442    fn request_layout(
443        &mut self,
444        _id: Option<&GlobalElementId>,
445        _inspector_id: Option<&InspectorElementId>,
446        window: &mut Window,
447        cx: &mut App,
448    ) -> (LayoutId, Self::RequestLayoutState) {
449        let viewport = self.coordinator.viewport_or_window(window.viewport_size());
450        let layers = self.coordinator.take_layer_elements();
451        let mut layer = deferred(
452            div()
453                .absolute()
454                .left(pixel_from_f64(viewport.x))
455                .top(pixel_from_f64(viewport.y))
456                .w(pixel_from_f64(viewport.width))
457                .h(pixel_from_f64(viewport.height))
458                .children(layers),
459        )
460        .with_priority(9_000)
461        .into_any_element();
462        let layout = layer.request_layout(window, cx);
463        (layout, layer)
464    }
465
466    fn prepaint(
467        &mut self,
468        _id: Option<&GlobalElementId>,
469        _inspector_id: Option<&InspectorElementId>,
470        _bounds: Bounds<Pixels>,
471        layer: &mut Self::RequestLayoutState,
472        window: &mut Window,
473        cx: &mut App,
474    ) {
475        layer.prepaint(window, cx);
476    }
477
478    fn paint(
479        &mut self,
480        _id: Option<&GlobalElementId>,
481        _inspector_id: Option<&InspectorElementId>,
482        _bounds: Bounds<Pixels>,
483        layer: &mut Self::RequestLayoutState,
484        _prepaint: &mut Self::PrepaintState,
485        window: &mut Window,
486        cx: &mut App,
487    ) {
488        layer.paint(window, cx);
489    }
490}
491
492impl IntoElement for SharedLayerPortalElement {
493    type Element = Self;
494
495    fn into_element(self) -> Self::Element {
496        self
497    }
498}
499
500#[derive(Clone)]
501pub struct ScriptViewHandle(Rc<ScriptViewHandleInner>);
502
503#[derive(Clone, Copy, Debug, Eq, PartialEq)]
504pub enum ScriptViewState {
505    Active,
506    Suspended,
507    Disposed,
508}
509
510#[derive(Clone)]
511pub struct ThemeHandle(Entity<ThemeHandleState>);
512
513struct ThemeHandleState {
514    snapshot: ThemeSnapshot,
515}
516
517impl ThemeHandleState {
518    fn replace(&mut self, variant: ThemeVariant) -> bool {
519        if self.snapshot.variant == variant {
520            return false;
521        }
522        self.snapshot = ThemeSnapshot::new(self.snapshot.revision.saturating_add(1), variant);
523        true
524    }
525}
526
527impl ThemeHandle {
528    fn new(variant: ThemeVariant, cx: &mut App) -> Self {
529        Self(cx.new(|_| ThemeHandleState {
530            snapshot: ThemeSnapshot::new(1, variant),
531        }))
532    }
533
534    /// Read the latest effective theme without invoking Rhai.
535    #[must_use]
536    pub fn snapshot(&self, cx: &App) -> ThemeSnapshot {
537        self.0.read(cx).snapshot.clone()
538    }
539
540    /// Observe effective-theme changes from application-level host code.
541    ///
542    /// The returned subscription must be retained for as long as observation is
543    /// required.
544    pub fn observe(
545        &self,
546        cx: &mut App,
547        mut on_change: impl FnMut(ThemeSnapshot, &mut App) + 'static,
548    ) -> Subscription {
549        cx.observe(&self.0, move |entity, cx| {
550            let snapshot = entity.read(cx).snapshot.clone();
551            on_change(snapshot, cx);
552        })
553    }
554
555    /// Observe effective-theme changes from another GPUI entity.
556    ///
557    /// This is the ergonomic path for Host-owned chrome: retain the returned
558    /// subscription in the observing entity and redraw only that entity.
559    pub fn observe_in<T: 'static>(
560        &self,
561        cx: &mut Context<T>,
562        mut on_change: impl FnMut(&mut T, ThemeSnapshot, &mut Context<T>) + 'static,
563    ) -> Subscription {
564        cx.observe(&self.0, move |owner, entity, cx| {
565            let snapshot = entity.read(cx).snapshot.clone();
566            on_change(owner, snapshot, cx);
567        })
568    }
569
570    fn matches(&self, variant: &ThemeVariant, cx: &App) -> bool {
571        &self.0.read(cx).snapshot.variant == variant
572    }
573
574    fn publish(&self, variant: ThemeVariant, cx: &mut App) {
575        self.0.update(cx, |state, cx| {
576            if state.replace(variant) {
577                cx.notify();
578            }
579        });
580    }
581}
582
583/// Drainable execution diagnostics for one mounted script view.
584///
585/// Taking a snapshot does not invoke Rhai or alter the mounted UI. Timings are
586/// drained so benchmark samples never double-count earlier work.
587#[derive(Clone, Debug)]
588pub struct ScriptViewPerformanceSnapshot {
589    pub timings: Vec<crate::ExecutionTiming>,
590    pub virtual_collections: Vec<crate::VirtualCollectionMetrics>,
591    pub retained_nodes: usize,
592    pub dirty_components: usize,
593    pub pending_virtual_requests: bool,
594}
595
596struct ScriptViewHandleInner {
597    entity: Entity<ScriptHostView>,
598    theme: ThemeHandle,
599    host: ScriptViewHost,
600    view_id: String,
601    state: Rc<Cell<ScriptViewState>>,
602    measured_bounds: Rc<Cell<Option<Bounds<Pixels>>>>,
603}
604
605impl Drop for ScriptViewHandleInner {
606    fn drop(&mut self) {
607        if self.state.get() != ScriptViewState::Disposed {
608            self.host.unregister_view(&self.view_id);
609        }
610    }
611}
612
613impl ScriptViewHandle {
614    fn require_not_disposed(&self) -> Result<ScriptViewState, ScriptViewError> {
615        let state = self.0.state.get();
616        if state == ScriptViewState::Disposed {
617            Err(ScriptViewError::DisposedView(self.0.view_id.clone()))
618        } else {
619            Ok(state)
620        }
621    }
622
623    fn require_active(&self) -> Result<(), ScriptViewError> {
624        match self.require_not_disposed()? {
625            ScriptViewState::Active => Ok(()),
626            ScriptViewState::Suspended => {
627                Err(ScriptViewError::SuspendedView(self.0.view_id.clone()))
628            }
629            ScriptViewState::Disposed => unreachable!(),
630        }
631    }
632
633    #[must_use]
634    pub fn view_id(&self) -> &str {
635        &self.0.view_id
636    }
637
638    #[must_use]
639    pub fn state(&self) -> ScriptViewState {
640        self.0.state.get()
641    }
642
643    /// Return a read-only, observable handle to this view's effective theme.
644    ///
645    /// # Errors
646    ///
647    /// Returns [`ScriptViewError::DisposedView`] after explicit disposal.
648    pub fn theme(&self) -> Result<ThemeHandle, ScriptViewError> {
649        self.require_not_disposed()?;
650        Ok(self.0.theme.clone())
651    }
652
653    /// Read this view's latest effective theme without invoking Rhai.
654    ///
655    /// # Errors
656    ///
657    /// Returns [`ScriptViewError::DisposedView`] after explicit disposal.
658    pub fn theme_snapshot(&self, cx: &App) -> Result<ThemeSnapshot, ScriptViewError> {
659        Ok(self.theme()?.snapshot(cx))
660    }
661
662    /// Return the latest rendered declarative root.
663    ///
664    /// # Errors
665    ///
666    /// Returns [`ScriptViewError::DisposedView`] after disposal.
667    /// Return the last successfully committed declarative tree.
668    ///
669    /// A failed render deliberately preserves this last-good tree. Pair this
670    /// method with [`Self::last_error`] when the caller must distinguish a
671    /// current successful tree from a rollback after failure.
672    pub fn root(&self, cx: &App) -> Result<Option<crate::UiNode>, ScriptViewError> {
673        self.require_not_disposed()?;
674        Ok(self.0.entity.read(cx).lifecycle.root().cloned())
675    }
676
677    /// Return the latest mounted-view render, callback, delivery, or reload error.
678    ///
679    /// Errors remain available while the last-good tree continues to render.
680    /// A later successful script transaction clears the value; native-only
681    /// repaint and animation frames do not.
682    ///
683    /// # Errors
684    ///
685    /// Returns [`ScriptViewError::DisposedView`] after disposal.
686    pub fn last_error(&self, cx: &App) -> Result<Option<String>, ScriptViewError> {
687        self.require_not_disposed()?;
688        Ok(self
689            .0
690            .entity
691            .read(cx)
692            .last_failure
693            .as_ref()
694            .map(|failure| failure.message.clone()))
695    }
696
697    /// Return structured details for the latest script runtime error.
698    ///
699    /// Non-script host errors only have a string representation and return
700    /// `None` here.
701    ///
702    /// # Errors
703    ///
704    /// Returns [`ScriptViewError::DisposedView`] after disposal.
705    pub fn last_diagnostic(&self, cx: &App) -> Result<Option<Diagnostic>, ScriptViewError> {
706        self.require_not_disposed()?;
707        Ok(self
708            .0
709            .entity
710            .read(cx)
711            .last_failure
712            .as_ref()
713            .and_then(|failure| failure.diagnostic.as_deref().cloned()))
714    }
715
716    /// Drain execution timings and snapshot retained/virtual metrics.
717    ///
718    /// # Errors
719    ///
720    /// Returns [`ScriptViewError::DisposedView`] after disposal.
721    pub fn take_performance_snapshot(
722        &self,
723        cx: &mut App,
724    ) -> Result<ScriptViewPerformanceSnapshot, ScriptViewError> {
725        self.require_not_disposed()?;
726        Ok(self.0.entity.update(cx, |view, _| {
727            let (virtual_collections, dirty_components, pending_virtual_requests) = {
728                let runtime_handle = view.lifecycle.runtime();
729                let runtime = runtime_handle.borrow();
730                (
731                    runtime.virtual_requests.inspect(),
732                    runtime.dirty_components().len(),
733                    runtime.has_virtual_requests(),
734                )
735            };
736            ScriptViewPerformanceSnapshot {
737                timings: std::mem::take(&mut view.timings),
738                virtual_collections,
739                retained_nodes: view.lifecycle.retained().len(),
740                dirty_components,
741                pending_virtual_requests,
742            }
743        }))
744    }
745
746    /// Read one mounted native hot value without invoking Rhai.
747    ///
748    /// # Errors
749    ///
750    /// Returns after disposal or when the signal is stale.
751    pub fn read_signal(
752        &self,
753        signal: &crate::NativeSignal,
754        cx: &App,
755    ) -> Result<crate::SignalValue, ScriptViewError> {
756        self.require_not_disposed()?;
757        Ok(self
758            .0
759            .entity
760            .read(cx)
761            .lifecycle
762            .runtime()
763            .borrow()
764            .signals
765            .read(signal)?)
766    }
767
768    /// Update one mounted native hot value on the GPUI foreground thread.
769    ///
770    /// This repaints signal-bound properties without running a Rhai component.
771    ///
772    /// # Errors
773    ///
774    /// Returns after disposal or for stale/type-incompatible values.
775    pub fn write_signal(
776        &self,
777        signal: &crate::NativeSignal,
778        value: crate::SignalValue,
779        cx: &mut App,
780    ) -> Result<bool, ScriptViewError> {
781        self.require_not_disposed()?;
782        self.0.entity.update(cx, |view, cx| {
783            let changed = view
784                .lifecycle
785                .runtime()
786                .borrow_mut()
787                .signals
788                .write(signal, value)?;
789            if changed {
790                cx.notify();
791            }
792            Ok::<_, ScriptViewError>(changed)
793        })
794    }
795
796    /// Replace one registered Rust-owned collection on the GPUI foreground thread.
797    ///
798    /// Only components that read this collection are marked dirty. The
799    /// replacement is immutable and can be prepared away from the Rhai runtime.
800    ///
801    /// # Errors
802    ///
803    /// Returns after disposal or when the collection name is unknown.
804    pub fn replace_native_collection(
805        &self,
806        name: &str,
807        collection: crate::NativeCollection,
808        cx: &mut App,
809    ) -> Result<bool, ScriptViewError> {
810        self.require_not_disposed()?;
811        self.0.entity.update(cx, |view, cx| {
812            let changed = view
813                .lifecycle
814                .runtime()
815                .borrow_mut()
816                .replace_native_collection_from_host(name, collection)?;
817            if changed {
818                cx.notify();
819            }
820            Ok::<_, ScriptViewError>(changed)
821        })
822    }
823
824    /// Replace one registered Host-owned text document revision.
825    ///
826    /// Only components that read this document are invalidated. The immutable
827    /// text snapshot may be prepared on a background thread before publication.
828    ///
829    /// # Errors
830    ///
831    /// Returns after disposal or when the document name is unknown.
832    pub fn replace_native_text_document(
833        &self,
834        name: &str,
835        document: crate::NativeTextDocument,
836        cx: &mut App,
837    ) -> Result<bool, ScriptViewError> {
838        self.require_not_disposed()?;
839        self.0.entity.update(cx, |view, cx| {
840            let changed = view
841                .lifecycle
842                .runtime()
843                .borrow_mut()
844                .replace_native_text_document_from_host(name, document)?;
845            if changed {
846                cx.notify();
847            }
848            Ok::<_, ScriptViewError>(changed)
849        })
850    }
851
852    /// Return the measured embedding element.
853    ///
854    /// # Errors
855    ///
856    /// Returns [`ScriptViewError::DisposedView`] after explicit disposal.
857    pub fn element(&self) -> Result<AnyElement, ScriptViewError> {
858        self.require_active()?;
859        Ok(MeasuredScriptViewElement {
860            entity: self.0.entity.clone(),
861            measured_bounds: Rc::clone(&self.0.measured_bounds),
862        }
863        .into_any_element())
864    }
865
866    /// Return the measured view as a zero-basis, shrinkable host flex item.
867    ///
868    /// Use this as the direct child of a Rust flex row or column. It prevents
869    /// horizontally scrollable script content from contributing an automatic
870    /// min-content width that expands the surrounding host layout.
871    ///
872    /// # Errors
873    ///
874    /// Returns [`ScriptViewError::DisposedView`] after explicit disposal.
875    pub fn flex_item(&self) -> Result<AnyElement, ScriptViewError> {
876        Ok(div()
877            .flex_1()
878            .min_w(px(0.0))
879            .min_h(px(0.0))
880            .debug_selector(|| format!("gpui-rhai-flex-item:{}", self.view_id()))
881            .child(self.element()?)
882            .into_any_element())
883    }
884
885    pub(crate) fn host_slot_item(&self) -> Result<AnyElement, ScriptViewError> {
886        let item = self.flex_item()?;
887        if self.0.host.frame_active() {
888            Ok(item)
889        } else {
890            Ok(self.0.host.container(item))
891        }
892    }
893
894    /// Quiesce this retained view without discarding script or native UI state.
895    ///
896    /// # Errors
897    ///
898    /// Returns after disposal or when a suspend hook/effect cleanup fails.
899    pub fn suspend(&self, window: &mut Window, cx: &mut App) -> Result<bool, ScriptViewError> {
900        match self.require_not_disposed()? {
901            ScriptViewState::Suspended => return Ok(false),
902            ScriptViewState::Active => {}
903            ScriptViewState::Disposed => unreachable!(),
904        }
905        self.0
906            .entity
907            .update(cx, |view, cx| view.suspend_view(window, cx))
908    }
909
910    /// Atomically resume a suspended view before returning it to Host layout.
911    ///
912    /// # Errors
913    ///
914    /// Returns after disposal or when pending delivery, resume hook, reload, or
915    /// the first restored render fails. A compensable failure leaves the view
916    /// suspended; failed compensation disposes and quarantines the view.
917    pub fn resume(&self, cx: &mut App) -> Result<bool, ScriptViewError> {
918        match self.require_not_disposed()? {
919            ScriptViewState::Active => return Ok(false),
920            ScriptViewState::Suspended => {}
921            ScriptViewState::Disposed => unreachable!(),
922        }
923        self.0.entity.update(cx, ScriptHostView::resume_view)
924    }
925
926    /// Dispose this view immediately. The operation is idempotent.
927    ///
928    /// # Errors
929    ///
930    /// Returns an entity update error only if GPUI has already released the
931    /// underlying view unexpectedly.
932    pub fn dispose(&self, cx: &mut App) -> Result<(), ScriptViewError> {
933        if self.0.state.get() == ScriptViewState::Disposed {
934            return Ok(());
935        }
936        self.0.entity.update(cx, |view, cx| {
937            view.release_view();
938            cx.notify();
939        });
940        self.0.host.unregister_view(&self.0.view_id);
941        cx.refresh_windows();
942        Ok(())
943    }
944
945    /// Focus this view's stable root handle.
946    ///
947    /// # Errors
948    ///
949    /// Returns [`ScriptViewError::DisposedView`] after disposal.
950    pub fn focus(&self, window: &mut Window, cx: &mut App) -> Result<(), ScriptViewError> {
951        self.require_active()?;
952        let focus = self.0.entity.read(cx).host_focus.clone();
953        focus.focus(window, cx);
954        Ok(())
955    }
956
957    /// Snapshot the retained semantic tree and last committed geometry.
958    ///
959    /// # Errors
960    ///
961    /// Returns after disposal or for an invalid retained semantic graph.
962    pub fn accessibility_snapshot(
963        &self,
964        cx: &App,
965    ) -> Result<crate::AccessibilityTree, ScriptViewError> {
966        self.require_active()?;
967        let view = self.0.entity.read(cx);
968        let geometry = view
969            .lifecycle
970            .runtime()
971            .borrow()
972            .geometry_for(Some(&view.view_id));
973        let mut tree = crate::AccessibilityTree::from_committed(
974            view.lifecycle.semantics(),
975            view.lifecycle.retained(),
976            &geometry,
977            true,
978        );
979        tree.apply_primitive_projections(
980            view.primitives
981                .accessibility_projections(view.lifecycle.retained(), cx),
982        );
983        Ok(tree)
984    }
985
986    /// Snapshot the stable language-neutral automation tree.
987    ///
988    /// # Errors
989    ///
990    /// Returns after disposal or for an invalid retained semantic graph.
991    pub fn automation_snapshot(
992        &self,
993        cx: &App,
994    ) -> Result<crate::AutomationSnapshot, ScriptViewError> {
995        Ok(crate::AutomationSnapshot::from_accessibility(
996            &self.accessibility_snapshot(cx)?,
997        ))
998    }
999
1000    /// Execute one automation command on the GPUI foreground thread.
1001    ///
1002    /// Dispatch and action commands use the mounted production handlers;
1003    /// deterministic time advance requires an injected controllable clock.
1004    ///
1005    /// # Errors
1006    ///
1007    /// Returns after disposal, for locator/command failures, or runtime errors.
1008    pub fn automate(
1009        &self,
1010        command: crate::AutomationCommand,
1011        window: &mut Window,
1012        cx: &mut App,
1013    ) -> Result<crate::AutomationResult, ScriptViewError> {
1014        self.require_active()?;
1015        match command {
1016            crate::AutomationCommand::Snapshot => Ok(crate::AutomationResult::Snapshot {
1017                snapshot: self.automation_snapshot(cx)?,
1018            }),
1019            crate::AutomationCommand::Query { locator } => {
1020                let view = self.0.entity.read(cx);
1021                let geometry = view
1022                    .lifecycle
1023                    .runtime()
1024                    .borrow()
1025                    .geometry_for(Some(&view.view_id));
1026                let mut tree = crate::AccessibilityTree::from_committed(
1027                    view.lifecycle.semantics(),
1028                    view.lifecycle.retained(),
1029                    &geometry,
1030                    true,
1031                );
1032                tree.apply_primitive_projections(
1033                    view.primitives
1034                        .accessibility_projections(view.lifecycle.retained(), cx),
1035                );
1036                let id = crate::automation::resolve_locator(&tree, &locator)?;
1037                let node = tree
1038                    .node(id)
1039                    .ok_or_else(|| crate::AutomationError::StaleTarget(id.get()))?;
1040                Ok(crate::AutomationResult::Node {
1041                    node: Box::new(crate::AutomationNode::from(node)),
1042                })
1043            }
1044            command => self
1045                .0
1046                .entity
1047                .update(cx, |view, cx| view.execute_automation(command, window, cx)),
1048        }
1049    }
1050
1051    /// Return the last successfully committed retained diff report.
1052    ///
1053    /// # Errors
1054    ///
1055    /// Returns after disposal.
1056    pub fn reconcile_report(&self, cx: &App) -> Result<crate::ReconcileReport, ScriptViewError> {
1057        self.require_not_disposed()?;
1058        Ok(self
1059            .0
1060            .entity
1061            .read(cx)
1062            .lifecycle
1063            .retained()
1064            .last_report()
1065            .clone())
1066    }
1067
1068    /// Focus a mounted retained element without invoking Rhai.
1069    ///
1070    /// # Errors
1071    ///
1072    /// Returns after disposal, for a stale ref, or when the ref has no active
1073    /// GPUI focus handle.
1074    pub fn focus_element(
1075        &self,
1076        reference: &crate::ElementRef,
1077        window: &mut Window,
1078        cx: &mut App,
1079    ) -> Result<(), ScriptViewError> {
1080        self.require_active()?;
1081        let handle = self.0.entity.update(cx, |view, _| {
1082            let node = view
1083                .lifecycle
1084                .runtime()
1085                .borrow()
1086                .element_refs
1087                .resolve(reference)?;
1088            view.focus_handles.get(&node).cloned().ok_or_else(|| {
1089                ScriptViewError::ElementRef(crate::ElementRefError::Stale(reference.id().clone()))
1090            })
1091        })?;
1092        handle.focus(window, cx);
1093        Ok(())
1094    }
1095
1096    #[cfg(feature = "dev-reload")]
1097    /// Open or close this view's isolated development inspector.
1098    ///
1099    /// # Errors
1100    ///
1101    /// Returns [`ScriptViewError::DisposedView`] after disposal.
1102    pub fn set_inspector_open(&self, open: bool, cx: &mut App) -> Result<(), ScriptViewError> {
1103        self.require_active()?;
1104        self.0.entity.update(cx, |view, cx| {
1105            view.inspector_open = open;
1106            cx.notify();
1107        });
1108        Ok(())
1109    }
1110}
1111
1112struct MeasuredScriptViewElement {
1113    entity: Entity<ScriptHostView>,
1114    measured_bounds: Rc<Cell<Option<Bounds<Pixels>>>>,
1115}
1116
1117impl Element for MeasuredScriptViewElement {
1118    type RequestLayoutState = AnyElement;
1119    type PrepaintState = ();
1120
1121    fn id(&self) -> Option<ElementId> {
1122        None
1123    }
1124
1125    fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
1126        None
1127    }
1128
1129    fn request_layout(
1130        &mut self,
1131        _id: Option<&GlobalElementId>,
1132        _inspector_id: Option<&InspectorElementId>,
1133        window: &mut Window,
1134        cx: &mut App,
1135    ) -> (LayoutId, Self::RequestLayoutState) {
1136        let mut child = self.entity.clone().into_any_element();
1137        let layout = child.request_layout(window, cx);
1138        (layout, child)
1139    }
1140
1141    fn prepaint(
1142        &mut self,
1143        _id: Option<&GlobalElementId>,
1144        _inspector_id: Option<&InspectorElementId>,
1145        bounds: Bounds<Pixels>,
1146        child: &mut Self::RequestLayoutState,
1147        window: &mut Window,
1148        cx: &mut App,
1149    ) {
1150        child.prepaint(window, cx);
1151        if self.measured_bounds.get() != Some(bounds) {
1152            self.measured_bounds.set(Some(bounds));
1153            let view = self.entity.downgrade();
1154            window.defer(cx, move |_, cx| {
1155                let _ = view.update(cx, |view, cx| view.set_content_bounds(bounds, cx));
1156            });
1157        }
1158    }
1159
1160    fn paint(
1161        &mut self,
1162        _id: Option<&GlobalElementId>,
1163        _inspector_id: Option<&InspectorElementId>,
1164        _bounds: Bounds<Pixels>,
1165        child: &mut Self::RequestLayoutState,
1166        _prepaint: &mut Self::PrepaintState,
1167        window: &mut Window,
1168        cx: &mut App,
1169    ) {
1170        child.paint(window, cx);
1171    }
1172}
1173
1174impl IntoElement for MeasuredScriptViewElement {
1175    type Element = Self;
1176
1177    fn into_element(self) -> Self::Element {
1178        self
1179    }
1180}
1181
1182pub trait ScriptViewExtension {
1183    /// Register custom primitives or engine APIs before scripts compile.
1184    ///
1185    /// # Errors
1186    ///
1187    /// Returns an application-facing extension diagnostic.
1188    fn configure_engine(&self, _engine: &mut RuntimeEngine) -> Result<(), String> {
1189        Ok(())
1190    }
1191
1192    /// Register and activate capabilities or stores before lifecycle init.
1193    ///
1194    /// # Errors
1195    ///
1196    /// Returns an application-facing extension diagnostic.
1197    fn configure_runtime(&self, _runtime: &mut UiRuntimeState) -> Result<(), String> {
1198        Ok(())
1199    }
1200
1201    /// Declare resources owned by each script window, such as window stores.
1202    /// This runs once before that window's lifecycle `init`.
1203    ///
1204    /// # Errors
1205    ///
1206    /// Returns an application-facing extension diagnostic.
1207    fn configure_window(
1208        &self,
1209        _window_id: &str,
1210        _runtime: &mut UiRuntimeState,
1211    ) -> Result<(), String> {
1212        Ok(())
1213    }
1214}
1215
1216fn motion_preference_from_env() -> MotionPreference {
1217    std::env::var("GPUI_RHAI_REDUCED_MOTION")
1218        .ok()
1219        .as_deref()
1220        .map_or(MotionPreference::Normal, parse_motion_preference)
1221}
1222
1223fn parse_motion_preference(value: &str) -> MotionPreference {
1224    if matches!(
1225        value.trim().to_ascii_lowercase().as_str(),
1226        "1" | "true" | "reduce" | "reduced"
1227    ) {
1228        MotionPreference::Reduced
1229    } else {
1230        MotionPreference::Normal
1231    }
1232}
1233
1234pub struct FileScriptView {
1235    entry: PathBuf,
1236    development: bool,
1237    motion_preference: MotionPreference,
1238    motion_quality: crate::MotionQuality,
1239    extensions: Vec<Box<dyn ScriptViewExtension>>,
1240    key_bindings: Vec<KeyBindingSpec>,
1241    viewport_breakpoints: ViewportBreakpoints,
1242    calendar_clock: crate::CalendarClock,
1243    runtime_clock: crate::RuntimeClock,
1244    fonts: Vec<crate::FontSource>,
1245    theme_token_overrides: ThemeTokenOverrides,
1246}
1247
1248impl FileScriptView {
1249    #[must_use]
1250    pub fn new(entry: impl Into<PathBuf>) -> Self {
1251        Self {
1252            entry: entry.into(),
1253            development: cfg!(feature = "dev-reload") && cfg!(debug_assertions),
1254            motion_preference: motion_preference_from_env(),
1255            motion_quality: crate::MotionQuality::High,
1256            extensions: Vec::new(),
1257            key_bindings: Vec::new(),
1258            viewport_breakpoints: ViewportBreakpoints::default(),
1259            calendar_clock: crate::CalendarClock::default(),
1260            runtime_clock: crate::RuntimeClock::default(),
1261            fonts: Vec::new(),
1262            theme_token_overrides: ThemeTokenOverrides::default(),
1263        }
1264    }
1265
1266    #[must_use]
1267    pub fn development(mut self, enabled: bool) -> Self {
1268        self.development = enabled;
1269        self
1270    }
1271
1272    #[must_use]
1273    pub const fn motion_preference(mut self, preference: MotionPreference) -> Self {
1274        self.motion_preference = preference;
1275        self
1276    }
1277
1278    #[must_use]
1279    pub const fn motion_quality(mut self, quality: crate::MotionQuality) -> Self {
1280        self.motion_quality = quality;
1281        self
1282    }
1283
1284    #[must_use]
1285    pub fn extension(mut self, extension: impl ScriptViewExtension + 'static) -> Self {
1286        self.extensions.push(Box::new(extension));
1287        self
1288    }
1289
1290    #[must_use]
1291    pub fn key_binding(mut self, binding: KeyBindingSpec) -> Self {
1292        self.key_bindings.push(binding);
1293        self
1294    }
1295
1296    #[must_use]
1297    pub const fn viewport_breakpoints(mut self, breakpoints: ViewportBreakpoints) -> Self {
1298        self.viewport_breakpoints = breakpoints;
1299        self
1300    }
1301
1302    #[must_use]
1303    pub fn calendar_clock(mut self, clock: crate::CalendarClock) -> Self {
1304        self.calendar_clock = clock;
1305        self
1306    }
1307
1308    #[must_use]
1309    pub fn runtime_clock(mut self, clock: crate::RuntimeClock) -> Self {
1310        self.runtime_clock = clock;
1311        self
1312    }
1313
1314    #[must_use]
1315    pub fn font_source(mut self, font: crate::FontSource) -> Self {
1316        self.fonts.push(font);
1317        self
1318    }
1319
1320    #[must_use]
1321    pub fn font_sources(mut self, fonts: impl IntoIterator<Item = crate::FontSource>) -> Self {
1322        self.fonts.extend(fonts);
1323        self
1324    }
1325
1326    /// Apply host-owned user preferences to every theme loaded by this view.
1327    #[must_use]
1328    pub fn theme_token_overrides(mut self, overrides: ThemeTokenOverrides) -> Self {
1329        self.theme_token_overrides = overrides;
1330        self
1331    }
1332
1333    /// Read, compile, initialize, and render the app before opening GPUI.
1334    ///
1335    /// # Errors
1336    ///
1337    /// Returns [`ScriptViewError`] for source I/O, compilation, lifecycle, or
1338    /// initial rendering failures.
1339    pub fn prepare(self) -> Result<PreparedScriptView, ScriptViewError> {
1340        let source = fs::read_to_string(&self.entry).map_err(|source| ScriptViewError::Io {
1341            path: self.entry.clone(),
1342            source,
1343        })?;
1344        let extensions = Rc::new(self.extensions);
1345        let mut engine = RuntimeEngine::new();
1346        for extension in extensions.iter() {
1347            extension
1348                .configure_engine(&mut engine)
1349                .map_err(ScriptViewError::Extension)?;
1350        }
1351        let (ui_root, theme_path, theme) =
1352            load_primary_file_theme(&engine, &self.entry, &self.theme_token_overrides)?;
1353        let style_path = ui_root.join("styles.rhai");
1354        let mut fonts = self.fonts;
1355        fonts.extend(load_file_fonts(&ui_root.join("fonts"))?);
1356        crate::validate_font_sources(&fonts)?;
1357        let manifest = load_file_manifest(&ui_root, &self.entry)?;
1358        let (module_cache, compiled) = engine.with_program_preparation(|engine| {
1359            let module_cache =
1360                configure_file_modules(engine, &ui_root, &self.entry, &theme_path, &style_path)?;
1361            let compiled =
1362                engine.compile_self_contained_named(&self.entry.to_string_lossy(), &source)?;
1363            Ok::<_, ScriptViewError>((module_cache, compiled))
1364        })?;
1365        #[cfg(not(feature = "dev-reload"))]
1366        let _ = &module_cache;
1367        let component_exports = engine.component_exports()?;
1368        let component_renderers = engine.component_renderer_snapshot()?;
1369        manifest.validate_components(&component_exports)?;
1370        let component_styles =
1371            load_file_component_styles(engine.engine(), &style_path, &component_exports)?;
1372        let state_schema = engine.root_state_schema(&compiled)?;
1373        let mut runtime_state = UiRuntimeState::new();
1374        runtime_state.motions = MotionRuntime::new(self.motion_preference);
1375        runtime_state.motions.set_quality(self.motion_quality);
1376        runtime_state.responsive = ResponsiveRuntime::new(self.viewport_breakpoints);
1377        runtime_state.calendar_clock = self.calendar_clock;
1378        runtime_state.clock = self.runtime_clock;
1379        runtime_state.locale = load_locale_directory(engine.engine(), &ui_root.join("locales"))?;
1380        runtime_state.theme = Some(load_theme_directory(
1381            engine.engine(),
1382            &ui_root.join("themes"),
1383            &theme,
1384            &self.theme_token_overrides,
1385        )?);
1386        runtime_state.replace_component_styles_from_host(component_styles);
1387        register_file_assets(&runtime_state, &ui_root)?;
1388        preload_component_assets(&runtime_state, &component_exports)?;
1389        for extension in extensions.iter() {
1390            extension
1391                .configure_runtime(&mut runtime_state)
1392                .map_err(ScriptViewError::Extension)?;
1393        }
1394        manifest.activate(&mut runtime_state.capabilities)?;
1395        let runtime = Rc::new(RefCell::new(runtime_state));
1396        let program = WindowProgram {
1397            compiled: compiled.clone(),
1398            state_schema: state_schema.clone(),
1399            component_exports,
1400            component_renderers,
1401        };
1402        let factory = Rc::new(ScriptWindowFactory {
1403            program: RefCell::new(program),
1404            runtime,
1405            extensions,
1406            theme: theme.clone(),
1407            #[cfg(feature = "dev-reload")]
1408            theme_token_overrides: self.theme_token_overrides.clone(),
1409            show_error_banner: Cell::new(true),
1410            #[cfg(feature = "dev-reload")]
1411            development: self.development,
1412        });
1413        Ok(PreparedScriptView {
1414            engine,
1415            theme,
1416            entry: self.entry,
1417            ui_root,
1418            theme_path,
1419            style_path,
1420            development: self.development,
1421            factory,
1422            key_bindings: self.key_bindings,
1423            fonts,
1424            #[cfg(feature = "dev-reload")]
1425            module_cache,
1426        })
1427    }
1428}
1429
1430pub struct EmbeddedScriptView {
1431    entry: ModuleId,
1432    scripts: EmbeddedScriptSource,
1433    theme_source: String,
1434    component_style_source: Option<String>,
1435    locales: Vec<(String, String)>,
1436    themes: Vec<(String, String)>,
1437    development: bool,
1438    motion_preference: MotionPreference,
1439    motion_quality: crate::MotionQuality,
1440    extensions: Vec<Box<dyn ScriptViewExtension>>,
1441    manifest: AppManifest,
1442    key_bindings: Vec<KeyBindingSpec>,
1443    assets: BTreeMap<String, AssetData>,
1444    viewport_breakpoints: ViewportBreakpoints,
1445    calendar_clock: crate::CalendarClock,
1446    runtime_clock: crate::RuntimeClock,
1447    fonts: Vec<crate::FontSource>,
1448    theme_token_overrides: ThemeTokenOverrides,
1449}
1450
1451impl EmbeddedScriptView {
1452    #[must_use]
1453    pub fn new(
1454        entry: ModuleId,
1455        scripts: EmbeddedScriptSource,
1456        theme_source: impl Into<String>,
1457    ) -> Self {
1458        let manifest = AppManifest::new(entry.clone());
1459        Self {
1460            entry,
1461            scripts,
1462            theme_source: theme_source.into(),
1463            component_style_source: None,
1464            locales: Vec::new(),
1465            themes: Vec::new(),
1466            development: false,
1467            motion_preference: motion_preference_from_env(),
1468            motion_quality: crate::MotionQuality::High,
1469            extensions: Vec::new(),
1470            manifest,
1471            key_bindings: Vec::new(),
1472            assets: BTreeMap::new(),
1473            viewport_breakpoints: ViewportBreakpoints::default(),
1474            calendar_clock: crate::CalendarClock::default(),
1475            runtime_clock: crate::RuntimeClock::default(),
1476            fonts: Vec::new(),
1477            theme_token_overrides: ThemeTokenOverrides::default(),
1478        }
1479    }
1480
1481    #[must_use]
1482    pub fn locale_sources(mut self, locales: impl IntoIterator<Item = (String, String)>) -> Self {
1483        self.locales = locales.into_iter().collect();
1484        self
1485    }
1486
1487    #[must_use]
1488    pub fn theme_sources(mut self, themes: impl IntoIterator<Item = (String, String)>) -> Self {
1489        self.themes = themes.into_iter().collect();
1490        self
1491    }
1492
1493    /// Install one application-wide typed component stylesheet.
1494    #[must_use]
1495    pub fn component_styles(mut self, source: impl Into<String>) -> Self {
1496        self.component_style_source = Some(source.into());
1497        self
1498    }
1499
1500    #[must_use]
1501    pub const fn development(mut self, enabled: bool) -> Self {
1502        self.development = enabled;
1503        self
1504    }
1505
1506    #[must_use]
1507    pub const fn motion_preference(mut self, preference: MotionPreference) -> Self {
1508        self.motion_preference = preference;
1509        self
1510    }
1511
1512    #[must_use]
1513    pub const fn motion_quality(mut self, quality: crate::MotionQuality) -> Self {
1514        self.motion_quality = quality;
1515        self
1516    }
1517
1518    #[must_use]
1519    pub fn extension(mut self, extension: impl ScriptViewExtension + 'static) -> Self {
1520        self.extensions.push(Box::new(extension));
1521        self
1522    }
1523
1524    #[must_use]
1525    pub fn manifest(mut self, manifest: AppManifest) -> Self {
1526        self.manifest = manifest;
1527        self
1528    }
1529
1530    #[must_use]
1531    pub fn key_binding(mut self, binding: KeyBindingSpec) -> Self {
1532        self.key_bindings.push(binding);
1533        self
1534    }
1535
1536    #[must_use]
1537    pub fn asset_sources(mut self, assets: impl IntoIterator<Item = (String, AssetData)>) -> Self {
1538        self.assets = assets.into_iter().collect();
1539        self
1540    }
1541
1542    #[must_use]
1543    pub const fn viewport_breakpoints(mut self, breakpoints: ViewportBreakpoints) -> Self {
1544        self.viewport_breakpoints = breakpoints;
1545        self
1546    }
1547
1548    #[must_use]
1549    pub fn calendar_clock(mut self, clock: crate::CalendarClock) -> Self {
1550        self.calendar_clock = clock;
1551        self
1552    }
1553
1554    #[must_use]
1555    pub fn runtime_clock(mut self, clock: crate::RuntimeClock) -> Self {
1556        self.runtime_clock = clock;
1557        self
1558    }
1559
1560    #[must_use]
1561    pub fn font_source(mut self, font: crate::FontSource) -> Self {
1562        self.fonts.push(font);
1563        self
1564    }
1565
1566    #[must_use]
1567    pub fn font_sources(mut self, fonts: impl IntoIterator<Item = crate::FontSource>) -> Self {
1568        self.fonts.extend(fonts);
1569        self
1570    }
1571
1572    /// Apply host-owned user preferences to every embedded theme.
1573    #[must_use]
1574    pub fn theme_token_overrides(mut self, overrides: ThemeTokenOverrides) -> Self {
1575        self.theme_token_overrides = overrides;
1576        self
1577    }
1578
1579    /// Compile and initialize a fully embedded application.
1580    ///
1581    /// # Errors
1582    ///
1583    /// Returns source, theme, compile, or lifecycle errors.
1584    pub fn prepare(self) -> Result<PreparedScriptView, ScriptViewError> {
1585        crate::validate_font_sources(&self.fonts)?;
1586        let entry = self.scripts.load(&self.entry)?;
1587        validate_manifest_entry(&self.manifest, &self.entry)?;
1588        let extensions = Rc::new(self.extensions);
1589        let mut engine = RuntimeEngine::new();
1590        for extension in extensions.iter() {
1591            extension
1592                .configure_engine(&mut engine)
1593                .map_err(ScriptViewError::Extension)?;
1594        }
1595        let theme = load_theme_with_overrides(
1596            engine.engine(),
1597            "<embedded-theme>",
1598            &self.theme_source,
1599            &self.theme_token_overrides,
1600        )?;
1601        engine.set_module_resolver(RestrictedModuleResolver::from_source(&self.scripts)?);
1602        let compiled = engine.with_program_preparation(|engine| {
1603            preload_component_modules(engine, self.scripts.module_ids())?;
1604            engine
1605                .compile_self_contained_named(self.entry.as_str(), &entry.source)
1606                .map_err(ScriptViewError::from)
1607        })?;
1608        let component_exports = engine.component_exports()?;
1609        let component_renderers = engine.component_renderer_snapshot()?;
1610        self.manifest.validate_components(&component_exports)?;
1611        let component_styles = self.component_style_source.map_or_else(
1612            || Ok(ComponentStyleSheet::default()),
1613            |source| {
1614                load_component_styles(
1615                    engine.engine(),
1616                    "<embedded-component-styles>",
1617                    &source,
1618                    &component_exports,
1619                )
1620            },
1621        )?;
1622        let state_schema = engine.root_state_schema(&compiled)?;
1623        let mut runtime_state = UiRuntimeState::new();
1624        runtime_state.motions = MotionRuntime::new(self.motion_preference);
1625        runtime_state.motions.set_quality(self.motion_quality);
1626        runtime_state.responsive = ResponsiveRuntime::new(self.viewport_breakpoints);
1627        runtime_state.calendar_clock = self.calendar_clock;
1628        runtime_state.clock = self.runtime_clock;
1629        runtime_state.locale = load_embedded_locales(engine.engine(), self.locales)?;
1630        runtime_state.theme = Some(load_embedded_themes(
1631            engine.engine(),
1632            self.themes,
1633            &theme,
1634            &self.theme_token_overrides,
1635        )?);
1636        runtime_state.replace_component_styles_from_host(component_styles);
1637        if !self.assets.is_empty() {
1638            runtime_state
1639                .assets
1640                .register("app", InMemoryAssetProvider::new(self.assets))?;
1641        }
1642        preload_component_assets(&runtime_state, &component_exports)?;
1643        for extension in extensions.iter() {
1644            extension
1645                .configure_runtime(&mut runtime_state)
1646                .map_err(ScriptViewError::Extension)?;
1647        }
1648        self.manifest.activate(&mut runtime_state.capabilities)?;
1649        let runtime = Rc::new(RefCell::new(runtime_state));
1650        let program = WindowProgram {
1651            compiled: compiled.clone(),
1652            state_schema: state_schema.clone(),
1653            component_exports,
1654            component_renderers,
1655        };
1656        let factory = Rc::new(ScriptWindowFactory {
1657            program: RefCell::new(program),
1658            runtime,
1659            extensions,
1660            theme: theme.clone(),
1661            #[cfg(feature = "dev-reload")]
1662            theme_token_overrides: self.theme_token_overrides.clone(),
1663            show_error_banner: Cell::new(true),
1664            #[cfg(feature = "dev-reload")]
1665            development: self.development,
1666        });
1667        Ok(PreparedScriptView {
1668            engine,
1669            theme,
1670            entry: PathBuf::new(),
1671            ui_root: PathBuf::new(),
1672            theme_path: PathBuf::new(),
1673            style_path: PathBuf::new(),
1674            development: self.development,
1675            factory,
1676            key_bindings: self.key_bindings,
1677            fonts: self.fonts,
1678            #[cfg(feature = "dev-reload")]
1679            module_cache: ModuleCompileCache::new(),
1680        })
1681    }
1682}
1683
1684fn load_embedded_locales(
1685    engine: &rhai::Engine,
1686    sources: Vec<(String, String)>,
1687) -> Result<Option<LocaleManager>, ScriptViewError> {
1688    let mut bundles = sources
1689        .into_iter()
1690        .map(|(name, source)| {
1691            load_locale_source(engine, &name, &source)
1692                .map_err(|error| ScriptViewError::Locale(error.to_string()))
1693        })
1694        .collect::<Result<Vec<_>, _>>()?;
1695    bundles.sort_by(|left, right| left.locale.cmp(&right.locale));
1696    if bundles.is_empty() {
1697        return Ok(None);
1698    }
1699    let fallback = bundles
1700        .iter()
1701        .find(|bundle| bundle.locale == "en")
1702        .unwrap_or(&bundles[0])
1703        .locale
1704        .clone();
1705    Ok(Some(
1706        LocaleManager::new(bundles, fallback.clone(), fallback)
1707            .map_err(|error| ScriptViewError::Locale(error.to_string()))?,
1708    ))
1709}
1710
1711fn load_embedded_themes(
1712    engine: &rhai::Engine,
1713    sources: Vec<(String, String)>,
1714    primary: &ThemeVariant,
1715    overrides: &ThemeTokenOverrides,
1716) -> Result<ThemeManager, ScriptViewError> {
1717    let mut variants = BTreeMap::from([(
1718        (primary.family.clone(), primary.name.clone()),
1719        primary.clone(),
1720    )]);
1721    for (name, source) in sources {
1722        let variant = load_theme_with_overrides(engine, &name, &source, overrides)?;
1723        insert_theme_variant(&mut variants, variant)?;
1724    }
1725    theme_manager(variants.into_values(), primary)
1726}
1727
1728fn module_id_from_path(root: &Path, path: &Path) -> Result<ModuleId, ScriptViewError> {
1729    let relative = path
1730        .strip_prefix(root)
1731        .map_err(|_| ScriptViewError::ModulePath(path.to_path_buf()))?;
1732    let id = relative
1733        .with_extension("")
1734        .components()
1735        .map(|component| component.as_os_str().to_string_lossy())
1736        .collect::<Vec<_>>()
1737        .join("/");
1738    Ok(ModuleId::parse(id)?)
1739}
1740
1741fn load_file_manifest(root: &Path, entry: &Path) -> Result<AppManifest, ScriptViewError> {
1742    let path = root.join("app.toml");
1743    let source = fs::read_to_string(&path).map_err(|source| ScriptViewError::Io {
1744        path: path.clone(),
1745        source,
1746    })?;
1747    let manifest: AppManifest =
1748        toml::from_str(&source).map_err(|error| ScriptViewError::Manifest(error.to_string()))?;
1749    let entry = module_id_from_path(root, entry)?;
1750    validate_manifest_entry(&manifest, &entry)?;
1751    Ok(manifest)
1752}
1753
1754fn load_primary_file_theme(
1755    engine: &RuntimeEngine,
1756    entry: &Path,
1757    overrides: &ThemeTokenOverrides,
1758) -> Result<(PathBuf, PathBuf, ThemeVariant), ScriptViewError> {
1759    let root = entry
1760        .parent()
1761        .unwrap_or_else(|| Path::new("."))
1762        .to_path_buf();
1763    let path = root.join("theme.rhai");
1764    let source = fs::read_to_string(&path).map_err(|source| ScriptViewError::Io {
1765        path: path.clone(),
1766        source,
1767    })?;
1768    let theme =
1769        load_theme_with_overrides(engine.engine(), &path.to_string_lossy(), &source, overrides)?;
1770    Ok((root, path, theme))
1771}
1772
1773fn load_file_component_styles(
1774    engine: &rhai::Engine,
1775    path: &Path,
1776    components: &ComponentRegistry,
1777) -> Result<ComponentStyleSheet, ScriptViewError> {
1778    match fs::read_to_string(path) {
1779        Ok(source) => Ok(load_component_styles(
1780            engine,
1781            &path.to_string_lossy(),
1782            &source,
1783            components,
1784        )?),
1785        Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
1786            Ok(ComponentStyleSheet::default())
1787        }
1788        Err(source) => Err(ScriptViewError::Io {
1789            path: path.to_path_buf(),
1790            source,
1791        }),
1792    }
1793}
1794
1795fn configure_file_modules(
1796    engine: &mut RuntimeEngine,
1797    root: &Path,
1798    entry: &Path,
1799    theme: &Path,
1800    styles: &Path,
1801) -> Result<ModuleCompileCache, ScriptViewError> {
1802    let modules = discover_modules(root, entry, theme, styles)?;
1803    let source = FileScriptSource::new(root, modules)?;
1804    let mut cache = ModuleCompileCache::new();
1805    cache.refresh(engine.engine(), &source, source.module_ids())?;
1806    engine.set_module_resolver(RestrictedModuleResolver::from_source_with_cache(
1807        &source, &cache,
1808    )?);
1809    preload_component_modules(engine, source.module_ids())?;
1810    Ok(cache)
1811}
1812
1813fn preload_component_modules(
1814    engine: &mut RuntimeEngine,
1815    modules: impl IntoIterator<Item = ModuleId>,
1816) -> Result<(), ScriptViewError> {
1817    let imports = modules
1818        .into_iter()
1819        .filter(|id| id.as_str().starts_with("components/"))
1820        .enumerate()
1821        .map(|(index, id)| format!("import \"{id}\" as component_{index};"))
1822        .collect::<Vec<_>>()
1823        .join("\n");
1824    if !imports.is_empty() {
1825        engine.compile_self_contained_named("<component-registry>", &imports)?;
1826    }
1827    Ok(())
1828}
1829
1830fn register_file_assets(runtime: &UiRuntimeState, root: &Path) -> Result<(), ScriptViewError> {
1831    let asset_root = root.join("assets");
1832    if asset_root.exists() {
1833        runtime
1834            .assets
1835            .register("app", DirectoryAssetProvider::new(&asset_root)?)?;
1836    }
1837    Ok(())
1838}
1839
1840fn load_file_fonts(directory: &Path) -> Result<Vec<crate::FontSource>, ScriptViewError> {
1841    if !directory.exists() {
1842        return Ok(Vec::new());
1843    }
1844    let mut paths = collect_font_paths(directory)?;
1845    paths.retain(|path| {
1846        path.extension()
1847            .and_then(|extension| extension.to_str())
1848            .is_some_and(|extension| {
1849                matches!(
1850                    extension.to_ascii_lowercase().as_str(),
1851                    "ttf" | "otf" | "ttc"
1852                )
1853            })
1854    });
1855    paths.sort();
1856    paths
1857        .into_iter()
1858        .map(|path| {
1859            let bytes = fs::read(&path).map_err(|error| crate::FontError::Io {
1860                path: path.display().to_string(),
1861                message: error.to_string(),
1862            })?;
1863            let label = path
1864                .file_name()
1865                .and_then(|name| name.to_str())
1866                .unwrap_or("font")
1867                .to_owned();
1868            crate::FontSource::new(label, bytes).map_err(ScriptViewError::from)
1869        })
1870        .collect()
1871}
1872
1873fn collect_font_paths(directory: &Path) -> Result<Vec<PathBuf>, ScriptViewError> {
1874    let mut pending = vec![directory.to_path_buf()];
1875    let mut files = Vec::new();
1876    while let Some(current) = pending.pop() {
1877        for entry in fs::read_dir(&current).map_err(|error| crate::FontError::Io {
1878            path: current.display().to_string(),
1879            message: error.to_string(),
1880        })? {
1881            let path = entry
1882                .map_err(|error| crate::FontError::Io {
1883                    path: current.display().to_string(),
1884                    message: error.to_string(),
1885                })?
1886                .path();
1887            if path.is_dir() {
1888                pending.push(path);
1889            } else {
1890                files.push(path);
1891            }
1892        }
1893    }
1894    Ok(files)
1895}
1896
1897fn install_declared_fonts(
1898    fonts: Vec<crate::FontSource>,
1899    cx: &mut App,
1900) -> Result<(), ScriptViewError> {
1901    if fonts.is_empty() {
1902        return Ok(());
1903    }
1904    let fresh = {
1905        let loaded = &cx.global::<ScriptRuntimeInstallation>().loaded_fonts;
1906        fonts
1907            .into_iter()
1908            .filter(|font| !loaded.contains(&font.fingerprint()))
1909            .collect::<Vec<_>>()
1910    };
1911    if fresh.is_empty() {
1912        return Ok(());
1913    }
1914    let fingerprints = fresh
1915        .iter()
1916        .map(crate::FontSource::fingerprint)
1917        .collect::<Vec<_>>();
1918    let bytes = fresh
1919        .into_iter()
1920        .map(|font| Cow::Owned(font.into_bytes()))
1921        .collect();
1922    cx.text_system()
1923        .add_fonts(bytes)
1924        .map_err(|error| crate::FontError::Load(error.to_string()))?;
1925    cx.global_mut::<ScriptRuntimeInstallation>()
1926        .loaded_fonts
1927        .extend(fingerprints);
1928    Ok(())
1929}
1930
1931fn preload_component_assets(
1932    runtime: &UiRuntimeState,
1933    components: &ComponentRegistry,
1934) -> Result<(), ScriptViewError> {
1935    let ids = components
1936        .iter()
1937        .flat_map(|(_, component)| component.metadata.assets.iter())
1938        .map(|asset| {
1939            let logical = Path::new(asset).with_extension("");
1940            let logical = logical
1941                .components()
1942                .map(|component| component.as_os_str().to_string_lossy())
1943                .collect::<Vec<_>>()
1944                .join("/");
1945            AssetId::parse(format!("app/{logical}")).map_err(ScriptViewError::Asset)
1946        })
1947        .collect::<Result<BTreeSet<_>, _>>()?;
1948    runtime.assets.preload_images(ids)?;
1949    Ok(())
1950}
1951
1952fn validate_manifest_entry(
1953    manifest: &AppManifest,
1954    entry: &ModuleId,
1955) -> Result<(), ScriptViewError> {
1956    if &manifest.entry == entry {
1957        Ok(())
1958    } else {
1959        Err(ScriptViewError::ManifestEntry {
1960            manifest: manifest.entry.clone(),
1961            host: entry.clone(),
1962        })
1963    }
1964}
1965
1966fn discover_modules(
1967    root: &Path,
1968    entry: &Path,
1969    theme: &Path,
1970    styles: &Path,
1971) -> Result<Vec<ModuleId>, ScriptViewError> {
1972    let mut pending = vec![root.to_path_buf()];
1973    let mut modules = Vec::new();
1974    while let Some(directory) = pending.pop() {
1975        for item in fs::read_dir(&directory).map_err(|source| ScriptViewError::Io {
1976            path: directory.clone(),
1977            source,
1978        })? {
1979            let item = item.map_err(|source| ScriptViewError::Io {
1980                path: directory.clone(),
1981                source,
1982            })?;
1983            let path = item.path();
1984            if path.is_dir() {
1985                if !matches!(
1986                    path.file_name().and_then(|name| name.to_str()),
1987                    Some("locales" | "themes")
1988                ) {
1989                    pending.push(path);
1990                }
1991            } else if path.extension().and_then(|extension| extension.to_str()) == Some("rhai")
1992                && path != entry
1993                && path != theme
1994                && path != styles
1995            {
1996                modules.push(module_id_from_path(root, &path)?);
1997            }
1998        }
1999    }
2000    modules.sort();
2001    Ok(modules)
2002}
2003
2004fn load_locale_directory(
2005    engine: &rhai::Engine,
2006    directory: &Path,
2007) -> Result<Option<LocaleManager>, ScriptViewError> {
2008    if !directory.exists() {
2009        return Ok(None);
2010    }
2011    let mut paths = fs::read_dir(directory)
2012        .map_err(|source| ScriptViewError::Io {
2013            path: directory.to_path_buf(),
2014            source,
2015        })?
2016        .map(|entry| {
2017            entry
2018                .map(|entry| entry.path())
2019                .map_err(|source| ScriptViewError::Io {
2020                    path: directory.to_path_buf(),
2021                    source,
2022                })
2023        })
2024        .collect::<Result<Vec<_>, _>>()?;
2025    paths.retain(|path| path.extension().and_then(|extension| extension.to_str()) == Some("rhai"));
2026    paths.sort();
2027    let mut bundles = Vec::<LocaleBundle>::new();
2028    for path in paths {
2029        let source = fs::read_to_string(&path).map_err(|source| ScriptViewError::Io {
2030            path: path.clone(),
2031            source,
2032        })?;
2033        bundles.push(
2034            load_locale_source(engine, &path.to_string_lossy(), &source)
2035                .map_err(|error| ScriptViewError::Locale(error.to_string()))?,
2036        );
2037    }
2038    if bundles.is_empty() {
2039        return Ok(None);
2040    }
2041    let fallback = bundles
2042        .iter()
2043        .find(|bundle| bundle.locale == "en")
2044        .unwrap_or(&bundles[0])
2045        .locale
2046        .clone();
2047    Ok(Some(
2048        LocaleManager::new(bundles, fallback.clone(), fallback)
2049            .map_err(|error| ScriptViewError::Locale(error.to_string()))?,
2050    ))
2051}
2052
2053fn load_theme_directory(
2054    engine: &rhai::Engine,
2055    directory: &Path,
2056    primary: &ThemeVariant,
2057    overrides: &ThemeTokenOverrides,
2058) -> Result<ThemeManager, ScriptViewError> {
2059    let mut variants = BTreeMap::from([(
2060        (primary.family.clone(), primary.name.clone()),
2061        primary.clone(),
2062    )]);
2063    if directory.exists() {
2064        let mut paths = fs::read_dir(directory)
2065            .map_err(|source| ScriptViewError::Io {
2066                path: directory.to_path_buf(),
2067                source,
2068            })?
2069            .map(|entry| {
2070                entry
2071                    .map(|entry| entry.path())
2072                    .map_err(|source| ScriptViewError::Io {
2073                        path: directory.to_path_buf(),
2074                        source,
2075                    })
2076            })
2077            .collect::<Result<Vec<_>, _>>()?;
2078        paths.retain(|path| {
2079            path.extension().and_then(|extension| extension.to_str()) == Some("rhai")
2080        });
2081        paths.sort();
2082        for path in paths {
2083            let source = fs::read_to_string(&path).map_err(|source| ScriptViewError::Io {
2084                path: path.clone(),
2085                source,
2086            })?;
2087            let variant =
2088                load_theme_with_overrides(engine, &path.to_string_lossy(), &source, overrides)?;
2089            insert_theme_variant(&mut variants, variant)?;
2090        }
2091    }
2092    theme_manager(variants.into_values(), primary)
2093}
2094
2095fn load_theme_with_overrides(
2096    engine: &rhai::Engine,
2097    source_name: &str,
2098    source: &str,
2099    overrides: &ThemeTokenOverrides,
2100) -> Result<ThemeVariant, ScriptViewError> {
2101    crate::theme::load_theme_source_with_overrides(engine, source_name, source, overrides)
2102        .map_err(|error| ScriptViewError::Theme(error.to_string()))
2103}
2104
2105fn insert_theme_variant(
2106    variants: &mut BTreeMap<(String, String), ThemeVariant>,
2107    variant: ThemeVariant,
2108) -> Result<(), ScriptViewError> {
2109    let key = (variant.family.clone(), variant.name.clone());
2110    if let Some(existing) = variants.get(&key) {
2111        if existing == &variant {
2112            return Ok(());
2113        }
2114        return Err(ScriptViewError::Theme(format!(
2115            "theme `{}/{}` is defined more than once with different tokens",
2116            key.0, key.1
2117        )));
2118    }
2119    variants.insert(key, variant);
2120    Ok(())
2121}
2122
2123fn theme_manager(
2124    variants: impl IntoIterator<Item = ThemeVariant>,
2125    primary: &ThemeVariant,
2126) -> Result<ThemeManager, ScriptViewError> {
2127    ThemeManager::from_variants(
2128        variants,
2129        ThemeSelection::new(primary.family.clone(), primary.name.clone()),
2130    )
2131    .map_err(|error| ScriptViewError::Theme(error.to_string()))
2132}
2133
2134#[derive(Clone)]
2135struct WindowProgram {
2136    compiled: CompiledUi,
2137    state_schema: ComponentStateSchema,
2138    component_exports: ComponentRegistry,
2139    component_renderers: BTreeMap<ModuleId, crate::engine::RegisteredComponentRender>,
2140}
2141
2142struct ScriptWindowFactory {
2143    program: RefCell<WindowProgram>,
2144    runtime: Rc<RefCell<UiRuntimeState>>,
2145    extensions: Rc<Vec<Box<dyn ScriptViewExtension>>>,
2146    theme: ThemeVariant,
2147    #[cfg(feature = "dev-reload")]
2148    theme_token_overrides: ThemeTokenOverrides,
2149    show_error_banner: Cell<bool>,
2150    #[cfg(feature = "dev-reload")]
2151    development: bool,
2152}
2153
2154impl ScriptWindowFactory {
2155    fn instantiate(
2156        &self,
2157        view_id: &str,
2158        window_id: &str,
2159    ) -> Result<(RuntimeEngine, ScriptLifecycle, PrimitiveRegistry), ScriptViewError> {
2160        let mut engine = RuntimeEngine::new();
2161        for extension in self.extensions.iter() {
2162            extension
2163                .configure_engine(&mut engine)
2164                .map_err(ScriptViewError::Extension)?;
2165        }
2166        let program = self.program.borrow().clone();
2167        engine.restore_component_exports(program.component_exports.clone())?;
2168        engine.restore_component_renderers(program.component_renderers.clone())?;
2169        let lifecycle = self.mount_lifecycle(
2170            &mut engine,
2171            program,
2172            view_id,
2173            window_id,
2174            WindowCommandPolicy::ApplicationOwned,
2175            false,
2176        )?;
2177        let primitives = engine.primitive_registry();
2178        Ok((engine, lifecycle, primitives))
2179    }
2180
2181    fn mount_lifecycle(
2182        &self,
2183        engine: &mut RuntimeEngine,
2184        program: WindowProgram,
2185        view_id: &str,
2186        window_id: &str,
2187        policy: WindowCommandPolicy,
2188        register_window: bool,
2189    ) -> Result<ScriptLifecycle, ScriptViewError> {
2190        {
2191            let mut runtime = self.runtime.borrow_mut();
2192            if register_window {
2193                runtime
2194                    .windows
2195                    .register_open_for_view(window_id, policy, view_id)
2196                    .map_err(|error| ScriptViewError::Extension(error.to_string()))?;
2197            }
2198            for extension in self.extensions.iter() {
2199                extension
2200                    .configure_window(window_id, &mut runtime)
2201                    .map_err(ScriptViewError::Extension)?;
2202            }
2203        }
2204        let mut lifecycle = ScriptLifecycle::new(
2205            program.compiled,
2206            Rc::clone(&self.runtime),
2207            ComponentInstancePath::root("View", view_id),
2208            Some(window_id.to_owned()),
2209            BTreeMap::new(),
2210            &program.state_schema,
2211        )?
2212        .with_view_id(view_id);
2213        if let Err(error) = lifecycle.start(engine) {
2214            let message = error.to_string();
2215            if let crate::LifecycleError::Runtime(runtime_error) = &error
2216                && let Ok(context) = lifecycle.diagnostic_context(engine, None)
2217            {
2218                return Err(ScriptViewError::ScriptDiagnostic {
2219                    message,
2220                    diagnostic: Box::new(Diagnostic::from_runtime(runtime_error, &context)),
2221                });
2222            }
2223            return Err(ScriptViewError::Lifecycle(error));
2224        }
2225        Ok(lifecycle)
2226    }
2227
2228    #[cfg(feature = "dev-reload")]
2229    fn update_program(
2230        &self,
2231        compiled: CompiledUi,
2232        state_schema: ComponentStateSchema,
2233        component_exports: ComponentRegistry,
2234        component_renderers: BTreeMap<ModuleId, crate::engine::RegisteredComponentRender>,
2235    ) {
2236        *self.program.borrow_mut() = WindowProgram {
2237            compiled,
2238            state_schema,
2239            component_exports,
2240            component_renderers,
2241        };
2242    }
2243
2244    fn program(&self) -> WindowProgram {
2245        self.program.borrow().clone()
2246    }
2247}
2248
2249#[derive(Default)]
2250struct NativeWindowRegistry {
2251    handles: BTreeMap<String, AnyWindowHandle>,
2252    force_close: std::collections::BTreeSet<String>,
2253}
2254
2255pub struct PreparedScriptView {
2256    engine: RuntimeEngine,
2257    theme: ThemeVariant,
2258    entry: PathBuf,
2259    ui_root: PathBuf,
2260    theme_path: PathBuf,
2261    style_path: PathBuf,
2262    development: bool,
2263    factory: Rc<ScriptWindowFactory>,
2264    key_bindings: Vec<KeyBindingSpec>,
2265    fonts: Vec<crate::FontSource>,
2266    #[cfg(feature = "dev-reload")]
2267    module_cache: ModuleCompileCache,
2268}
2269
2270impl PreparedScriptView {
2271    #[must_use]
2272    pub fn key_bindings(&self) -> &[KeyBindingSpec] {
2273        &self.key_bindings
2274    }
2275
2276    /// Mount one isolated script view into an existing GPUI window.
2277    ///
2278    /// # Errors
2279    ///
2280    /// Returns identity, lifecycle, extension, or watcher errors.
2281    pub fn mount(
2282        self,
2283        config: ScriptViewConfig,
2284        host: ScriptViewHost,
2285        window: &mut Window,
2286        cx: &mut App,
2287    ) -> Result<ScriptViewHandle, ScriptViewError> {
2288        self.mount_with_registry(
2289            config,
2290            host,
2291            Rc::new(RefCell::new(NativeWindowRegistry::default())),
2292            window,
2293            cx,
2294        )
2295    }
2296
2297    fn mount_with_registry(
2298        mut self,
2299        config: ScriptViewConfig,
2300        host: ScriptViewHost,
2301        native_windows: Rc<RefCell<NativeWindowRegistry>>,
2302        window: &mut Window,
2303        cx: &mut App,
2304    ) -> Result<ScriptViewHandle, ScriptViewError> {
2305        install(cx);
2306        install_declared_fonts(std::mem::take(&mut self.fonts), cx)?;
2307        #[cfg(feature = "dev-reload")]
2308        let watcher = development_watcher(self.development, &self.ui_root)?;
2309        host.reserve_view(&config.view_id)?;
2310        let window_id = host.window_id();
2311        let lifecycle = mount_prepared_lifecycle(
2312            &self.factory,
2313            &mut self.engine,
2314            &host,
2315            &config.view_id,
2316            &window_id,
2317        )?;
2318        #[cfg(not(feature = "dev-reload"))]
2319        let _ = (
2320            &self.entry,
2321            &self.ui_root,
2322            &self.theme_path,
2323            &self.style_path,
2324            self.development,
2325        );
2326        let primitives = self.engine.primitive_registry();
2327        let timings = self.engine.take_timings();
2328        let factory = Rc::clone(&self.factory);
2329        let overlays = host.overlays();
2330        let view_id = config.view_id.clone();
2331        let view_host = host.clone();
2332        let (theme_handle, view_theme_handle) =
2333            theme_handles_for_lifecycle(&lifecycle, &self.theme, window, cx);
2334        let view_state = Rc::new(Cell::new(ScriptViewState::Active));
2335        let entity_view_state = Rc::clone(&view_state);
2336        let entity_activity_wake = crate::async_runtime::AsyncWake::default();
2337        let entity = cx.new(|entity_cx| {
2338            let runtime_tasks = spawn_host_runtime_tasks(
2339                entity_cx,
2340                &lifecycle,
2341                Rc::clone(&entity_view_state),
2342                entity_activity_wake.clone(),
2343            );
2344            let host_focus = entity_cx.focus_handle();
2345            #[cfg(feature = "dev-reload")]
2346            let reload_task = watcher
2347                .as_ref()
2348                .map(|_| spawn_host_reload_poll(entity_cx, Rc::clone(&entity_view_state)));
2349            ScriptHostView {
2350                view_id: view_id.clone(),
2351                window_id,
2352                engine: self.engine,
2353                lifecycle,
2354                primitives,
2355                last_failure: None,
2356                theme: self.theme,
2357                theme_handle: view_theme_handle,
2358                #[cfg(feature = "dev-reload")]
2359                development: self.development,
2360                #[cfg(feature = "dev-reload")]
2361                inspector_open: false,
2362                timings,
2363                overlays,
2364                host: view_host,
2365                paint_background: config.paint_background,
2366                show_error_banner: config.show_error_banner,
2367                content_bounds: None,
2368                factory,
2369                native_windows,
2370                host_focus,
2371                focus_handles: BTreeMap::new(),
2372                scroll_handles: BTreeMap::new(),
2373                scroll_anchors: BTreeMap::new(),
2374                text_selection: crate::renderer::TextSelectionRegistry::default(),
2375                last_motion_sample: None,
2376                state: entity_view_state,
2377                activity_wake: entity_activity_wake,
2378                _runtime_tasks: runtime_tasks,
2379                #[cfg(feature = "dev-reload")]
2380                module_cache: self.module_cache,
2381                #[cfg(feature = "dev-reload")]
2382                watcher,
2383                #[cfg(feature = "dev-reload")]
2384                entry: self.entry,
2385                #[cfg(feature = "dev-reload")]
2386                ui_root: self.ui_root,
2387                #[cfg(feature = "dev-reload")]
2388                theme_path: self.theme_path,
2389                #[cfg(feature = "dev-reload")]
2390                style_path: self.style_path,
2391                #[cfg(feature = "dev-reload")]
2392                _reload_task: reload_task,
2393                #[cfg(feature = "dev-reload")]
2394                pending_reload_paths: BTreeSet::new(),
2395            }
2396        });
2397        attach_script_view_focus(&host, &config.view_id, &entity, cx);
2398        Ok(mounted_script_view_handle(
2399            entity,
2400            theme_handle,
2401            host,
2402            config,
2403            view_state,
2404        ))
2405    }
2406}
2407
2408#[cfg(feature = "dev-reload")]
2409fn development_watcher(
2410    development: bool,
2411    ui_root: &Path,
2412) -> Result<Option<FileWatcher>, ScriptViewError> {
2413    if development && !ui_root.as_os_str().is_empty() {
2414        Ok(Some(FileWatcher::new(ui_root)?))
2415    } else {
2416        Ok(None)
2417    }
2418}
2419
2420type WindowOptionsConfigurator =
2421    Box<dyn FnOnce(WindowOptions, &mut App) -> WindowOptions + 'static>;
2422
2423pub struct ScriptApplication {
2424    prepared: PreparedScriptView,
2425    window_size: (f32, f32),
2426    window_options: Option<WindowOptionsConfigurator>,
2427    show_error_banner: bool,
2428}
2429
2430impl ScriptApplication {
2431    #[must_use]
2432    pub const fn new(prepared: PreparedScriptView) -> Self {
2433        Self {
2434            prepared,
2435            window_size: (720.0, 480.0),
2436            window_options: None,
2437            show_error_banner: true,
2438        }
2439    }
2440
2441    #[must_use]
2442    pub const fn window_size(mut self, width: f32, height: f32) -> Self {
2443        self.window_size = (width, height);
2444        self
2445    }
2446
2447    /// Control built-in runtime-error banners in standalone script windows.
2448    ///
2449    /// Applications that disable them must provide another visible surface for
2450    /// [`ScriptViewHandle::last_error`].
2451    #[must_use]
2452    pub const fn show_error_banner(mut self, show: bool) -> Self {
2453        self.show_error_banner = show;
2454        self
2455    }
2456
2457    /// Configure the trusted standalone window without exposing native window
2458    /// authority to Rhai. The callback receives the centered default options.
2459    #[must_use]
2460    pub fn window_options(
2461        mut self,
2462        configure: impl FnOnce(WindowOptions, &mut App) -> WindowOptions + 'static,
2463    ) -> Self {
2464        self.window_options = Some(Box::new(configure));
2465        self
2466    }
2467
2468    /// Run this prepared view as a standalone GPUI application.
2469    ///
2470    /// # Errors
2471    ///
2472    /// Returns key-binding, mount, or native-window errors.
2473    pub fn run(self) -> Result<(), ScriptViewError> {
2474        let error = Rc::new(RefCell::new(None));
2475        let reported_error = Rc::clone(&error);
2476        let native_windows = Rc::new(RefCell::new(NativeWindowRegistry::default()));
2477        let window_size = self.window_size;
2478        let window_options = self.window_options;
2479        let prepared = self.prepared;
2480        let show_error_banner = self.show_error_banner;
2481        prepared.factory.show_error_banner.set(show_error_banner);
2482        gpui_platform::application().run(move |cx: &mut App| {
2483            install(cx);
2484            let host = match ScriptViewHost::new_with_policy(
2485                "main",
2486                WindowCommandPolicy::ApplicationOwned,
2487                cx,
2488            ) {
2489                Ok(host) => host,
2490                Err(error) => {
2491                    *reported_error.borrow_mut() = Some(error.to_string());
2492                    cx.quit();
2493                    return;
2494                }
2495            };
2496            if let Err(error) = host.bind_keys(prepared.key_bindings.clone(), cx) {
2497                *reported_error.borrow_mut() = Some(error.to_string());
2498                cx.quit();
2499                return;
2500            }
2501            #[cfg(feature = "dev-reload")]
2502            cx.bind_keys([
2503                KeyBinding::new("cmd-alt-i", ToggleInspector, Some(HOST_KEY_CONTEXT)),
2504                KeyBinding::new("f12", ToggleInspector, Some(HOST_KEY_CONTEXT)),
2505            ]);
2506            let window_options = standalone_window_options(window_size, window_options, cx);
2507            cx.on_window_closed(|cx, _| {
2508                if cx.windows().is_empty() {
2509                    cx.quit();
2510                }
2511            })
2512            .detach();
2513            let view_native_windows = Rc::clone(&native_windows);
2514            let mount_error = Rc::clone(&reported_error);
2515            let result = cx.open_window(window_options, move |window, cx| {
2516                let root = cx.new(|_| ScriptApplicationRoot {
2517                    host: host.clone(),
2518                    view: None,
2519                    error: None,
2520                });
2521                let weak_root = root.downgrade();
2522                window.defer(cx, move |window, cx| {
2523                    let view = prepared.mount_with_registry(
2524                        ScriptViewConfig::new("main")
2525                            .paint_background(true)
2526                            .show_error_banner(show_error_banner),
2527                        host,
2528                        Rc::clone(&view_native_windows),
2529                        window,
2530                        cx,
2531                    );
2532                    match view {
2533                        Ok(view) => {
2534                            let _ = view.focus(window, cx);
2535                            install_close_interceptor(window, cx, &view.0.entity);
2536                            let _ = weak_root.update(cx, |root, cx| {
2537                                root.view = Some(view);
2538                                cx.notify();
2539                            });
2540                        }
2541                        Err(error) => {
2542                            let message = error.to_string();
2543                            *mount_error.borrow_mut() = Some(message.clone());
2544                            let _ = weak_root.update(cx, |root, cx| {
2545                                root.error = Some(message);
2546                                cx.notify();
2547                            });
2548                            cx.defer(|cx| cx.quit());
2549                        }
2550                    }
2551                });
2552                root
2553            });
2554            match result {
2555                Ok(handle) => {
2556                    native_windows
2557                        .borrow_mut()
2558                        .handles
2559                        .insert("main".to_owned(), handle.into());
2560                    cx.activate(true);
2561                }
2562                Err(error) => {
2563                    *reported_error.borrow_mut() = Some(error.to_string());
2564                    cx.quit();
2565                }
2566            }
2567        });
2568        match error.borrow_mut().take() {
2569            Some(message) => Err(ScriptViewError::Window(message)),
2570            None => Ok(()),
2571        }
2572    }
2573}
2574
2575fn standalone_window_options(
2576    window_size: (f32, f32),
2577    configure: Option<WindowOptionsConfigurator>,
2578    cx: &mut App,
2579) -> WindowOptions {
2580    let bounds = Bounds::centered(None, size(px(window_size.0), px(window_size.1)), cx);
2581    let options = WindowOptions {
2582        window_bounds: Some(WindowBounds::Windowed(bounds)),
2583        ..WindowOptions::default()
2584    };
2585    match configure {
2586        Some(configure) => configure(options, cx),
2587        None => options,
2588    }
2589}
2590
2591struct ScriptApplicationRoot {
2592    host: ScriptViewHost,
2593    view: Option<ScriptViewHandle>,
2594    error: Option<String>,
2595}
2596
2597impl Render for ScriptApplicationRoot {
2598    fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
2599        self.host.container(self.view.as_ref().map_or_else(
2600            || {
2601                self.error.as_ref().map_or_else(
2602                    || div().child("Loading…").into_any_element(),
2603                    |error| div().child(error.clone()).into_any_element(),
2604                )
2605            },
2606            |view| {
2607                view.element()
2608                    .unwrap_or_else(|error| div().child(error.to_string()).into_any_element())
2609            },
2610        ))
2611    }
2612}
2613
2614#[allow(clippy::too_many_lines)]
2615fn open_secondary_window(
2616    spec: &ScriptWindowSpec,
2617    factory: &Rc<ScriptWindowFactory>,
2618    native_windows: &Rc<RefCell<NativeWindowRegistry>>,
2619    cx: &mut App,
2620) -> Result<(), ScriptFailure> {
2621    let factory = Rc::clone(factory);
2622    let native_windows = Rc::clone(native_windows);
2623    let window_id = spec.id.clone();
2624    let host = ScriptViewHost::new_with_policy(
2625        window_id.clone(),
2626        WindowCommandPolicy::ApplicationOwned,
2627        cx,
2628    )
2629    .map_err(ScriptFailure::from)?;
2630    host.reserve_view(&window_id).map_err(ScriptFailure::from)?;
2631    let (engine, lifecycle, primitives) = match factory.instantiate(&window_id, &window_id) {
2632        Ok(instance) => instance,
2633        Err(error) => {
2634            host.unregister_view(&window_id);
2635            let root = ComponentInstancePath::root("View", &window_id);
2636            let _ = factory
2637                .runtime
2638                .borrow_mut()
2639                .release_window(&window_id, &root);
2640            return Err(error.into());
2641        }
2642    };
2643    let timings = engine.take_timings();
2644    let options = script_window_options(spec, cx);
2645    let view_factory = Rc::clone(&factory);
2646    let view_native_windows = Rc::clone(&native_windows);
2647    let view_window_id = window_id.clone();
2648    let view_host = host.clone();
2649    let result = cx.open_window(options, move |window, cx| {
2650        let (theme_handle, view_theme_handle) =
2651            theme_handles_for_lifecycle(&lifecycle, &view_factory.theme, window, cx);
2652        let view_state = Rc::new(Cell::new(ScriptViewState::Active));
2653        let entity_view_state = Rc::clone(&view_state);
2654        let entity_activity_wake = crate::async_runtime::AsyncWake::default();
2655        let entity = cx.new(|entity_cx| {
2656            let runtime_tasks = spawn_host_runtime_tasks(
2657                entity_cx,
2658                &lifecycle,
2659                Rc::clone(&entity_view_state),
2660                entity_activity_wake.clone(),
2661            );
2662            let host_focus = entity_cx.focus_handle();
2663            ScriptHostView {
2664                view_id: view_window_id.clone(),
2665                window_id: view_window_id.clone(),
2666                engine,
2667                lifecycle,
2668                primitives,
2669                last_failure: None,
2670                theme: view_factory.theme.clone(),
2671                theme_handle: view_theme_handle,
2672                #[cfg(feature = "dev-reload")]
2673                development: view_factory.development,
2674                #[cfg(feature = "dev-reload")]
2675                inspector_open: false,
2676                timings,
2677                overlays: view_host.overlays(),
2678                host: view_host.clone(),
2679                paint_background: true,
2680                show_error_banner: view_factory.show_error_banner.get(),
2681                content_bounds: None,
2682                factory: Rc::clone(&view_factory),
2683                native_windows: Rc::clone(&view_native_windows),
2684                host_focus,
2685                focus_handles: BTreeMap::new(),
2686                scroll_handles: BTreeMap::new(),
2687                scroll_anchors: BTreeMap::new(),
2688                text_selection: crate::renderer::TextSelectionRegistry::default(),
2689                last_motion_sample: None,
2690                state: entity_view_state,
2691                activity_wake: entity_activity_wake,
2692                _runtime_tasks: runtime_tasks,
2693                #[cfg(feature = "dev-reload")]
2694                module_cache: ModuleCompileCache::new(),
2695                #[cfg(feature = "dev-reload")]
2696                watcher: None,
2697                #[cfg(feature = "dev-reload")]
2698                entry: PathBuf::new(),
2699                #[cfg(feature = "dev-reload")]
2700                ui_root: PathBuf::new(),
2701                #[cfg(feature = "dev-reload")]
2702                theme_path: PathBuf::new(),
2703                #[cfg(feature = "dev-reload")]
2704                style_path: PathBuf::new(),
2705                #[cfg(feature = "dev-reload")]
2706                _reload_task: None,
2707                #[cfg(feature = "dev-reload")]
2708                pending_reload_paths: BTreeSet::new(),
2709            }
2710        });
2711        view_host.attach_view_focus(&view_window_id, entity.read(cx).host_focus.clone());
2712        let view = ScriptViewHandle(Rc::new(ScriptViewHandleInner {
2713            entity: entity.clone(),
2714            theme: theme_handle,
2715            host: view_host.clone(),
2716            view_id: view_window_id,
2717            state: view_state,
2718            measured_bounds: Rc::new(Cell::new(None)),
2719        }));
2720        let _ = view.focus(window, cx);
2721        install_close_interceptor(window, cx, &entity);
2722        cx.new(|_| ScriptApplicationRoot {
2723            host: view_host,
2724            view: Some(view),
2725            error: None,
2726        })
2727    });
2728    match result {
2729        Ok(handle) => {
2730            native_windows
2731                .borrow_mut()
2732                .handles
2733                .insert(window_id.clone(), handle.into());
2734            factory
2735                .runtime
2736                .borrow_mut()
2737                .windows
2738                .mark_open(&window_id)
2739                .map_err(|error| ScriptFailure::plain(error.to_string()))?;
2740            Ok(())
2741        }
2742        Err(error) => {
2743            host.unregister_view(&window_id);
2744            let root = ComponentInstancePath::root("View", &window_id);
2745            let mut runtime = factory.runtime.borrow_mut();
2746            let _ = runtime.release_window(&window_id, &root);
2747            Err(ScriptFailure::plain(error.to_string()))
2748        }
2749    }
2750}
2751
2752fn script_window_options(spec: &ScriptWindowSpec, cx: &mut App) -> WindowOptions {
2753    let bounds = Bounds::centered(
2754        None,
2755        size(
2756            px(validated_dimension_to_f32(spec.width)),
2757            px(validated_dimension_to_f32(spec.height)),
2758        ),
2759        cx,
2760    );
2761    WindowOptions {
2762        window_bounds: Some(WindowBounds::Windowed(bounds)),
2763        titlebar: Some(TitlebarOptions {
2764            title: Some(SharedString::from(spec.title.clone())),
2765            ..TitlebarOptions::default()
2766        }),
2767        focus: spec.focus,
2768        ..WindowOptions::default()
2769    }
2770}
2771
2772fn install_close_interceptor(window: &Window, cx: &App, entity: &gpui::Entity<ScriptHostView>) {
2773    let weak = entity.downgrade();
2774    window.on_window_should_close(cx, move |_, app| {
2775        weak.update(app, ScriptHostView::should_close)
2776            .unwrap_or(true)
2777    });
2778}
2779
2780struct HostRuntimeTasks {
2781    _frame_poll: Task<()>,
2782    _task_delivery: Task<()>,
2783    _subscription_delivery: Task<()>,
2784}
2785
2786fn spawn_host_runtime_tasks(
2787    cx: &mut Context<ScriptHostView>,
2788    lifecycle: &ScriptLifecycle,
2789    state: Rc<Cell<ScriptViewState>>,
2790    activity_wake: crate::async_runtime::AsyncWake,
2791) -> HostRuntimeTasks {
2792    let (task_wake, subscription_wake) = {
2793        let runtime = lifecycle.runtime();
2794        let runtime = runtime.borrow();
2795        (runtime.tasks.wake(), runtime.subscriptions.wake())
2796    };
2797    HostRuntimeTasks {
2798        _frame_poll: spawn_host_frame_poll(cx, state, activity_wake),
2799        _task_delivery: spawn_host_delivery_pump(cx, task_wake),
2800        _subscription_delivery: spawn_host_delivery_pump(cx, subscription_wake),
2801    }
2802}
2803
2804fn spawn_host_frame_poll(
2805    cx: &mut Context<ScriptHostView>,
2806    state: Rc<Cell<ScriptViewState>>,
2807    activity_wake: crate::async_runtime::AsyncWake,
2808) -> Task<()> {
2809    let mut listener = activity_wake.listen();
2810    cx.spawn(async move |entity: gpui::WeakEntity<ScriptHostView>, cx| {
2811        loop {
2812            match state.get() {
2813                ScriptViewState::Disposed => break,
2814                ScriptViewState::Suspended => {
2815                    listener.await;
2816                    listener = activity_wake.listen();
2817                    continue;
2818                }
2819                ScriptViewState::Active => {}
2820            }
2821            cx.background_executor()
2822                .timer(Duration::from_millis(16))
2823                .await;
2824            if entity.update(cx, ScriptHostView::poll_async).is_err() {
2825                break;
2826            }
2827        }
2828    })
2829}
2830
2831fn spawn_host_delivery_pump(
2832    cx: &mut Context<ScriptHostView>,
2833    wake: crate::async_runtime::AsyncWake,
2834) -> Task<()> {
2835    // Register before the entity becomes externally visible. The first drain
2836    // catches work that completed during lifecycle initialization; thereafter
2837    // the listener closes the send/drain race without fixed-rate polling.
2838    let mut listener = wake.listen();
2839    cx.spawn(async move |entity: gpui::WeakEntity<ScriptHostView>, cx| {
2840        loop {
2841            if entity.update(cx, ScriptHostView::poll_async).is_err() {
2842                break;
2843            }
2844            listener.await;
2845            listener = wake.listen();
2846        }
2847    })
2848}
2849
2850#[cfg(feature = "dev-reload")]
2851fn spawn_host_reload_poll(
2852    cx: &mut Context<ScriptHostView>,
2853    state: Rc<Cell<ScriptViewState>>,
2854) -> Task<()> {
2855    cx.spawn(async move |entity: gpui::WeakEntity<ScriptHostView>, cx| {
2856        loop {
2857            if state.get() == ScriptViewState::Disposed {
2858                break;
2859            }
2860            cx.background_executor()
2861                .timer(Duration::from_millis(100))
2862                .await;
2863            if entity.update(cx, ScriptHostView::poll_reload).is_err() {
2864                break;
2865            }
2866        }
2867    })
2868}
2869
2870#[allow(clippy::cast_possible_truncation)]
2871fn validated_dimension_to_f32(value: f64) -> f32 {
2872    debug_assert!(value.is_finite() && (200.0..=4096.0).contains(&value));
2873    value as f32
2874}
2875
2876fn pixel_from_f64(value: f64) -> Pixels {
2877    px(value.to_string().parse::<f32>().unwrap_or(f32::MAX))
2878}
2879
2880fn event_response_from_dynamic(value: &rhai::Dynamic) -> crate::EventResponse {
2881    if value.is::<crate::EventResponse>() {
2882        value.clone_cast::<crate::EventResponse>()
2883    } else if value.is::<rhai::ImmutableString>()
2884        && value.clone_cast::<rhai::ImmutableString>().as_str() == "propagate"
2885    {
2886        crate::EventResponse::new()
2887    } else {
2888        crate::EventResponse::new().stop()
2889    }
2890}
2891
2892fn automation_pointer_id(payload: &UiValue) -> Option<u64> {
2893    let UiValue::Map(payload) = payload else {
2894        return None;
2895    };
2896    match payload.get("pointer_id") {
2897        Some(UiValue::Integer(id)) => u64::try_from(*id).ok(),
2898        _ => None,
2899    }
2900}
2901
2902#[derive(Clone, Debug)]
2903struct ScriptFailure {
2904    message: String,
2905    diagnostic: Option<Box<Diagnostic>>,
2906}
2907
2908impl ScriptFailure {
2909    fn plain(message: impl Into<String>) -> Self {
2910        Self {
2911            message: message.into(),
2912            diagnostic: None,
2913        }
2914    }
2915
2916    fn with_diagnostic(message: impl Into<String>, diagnostic: Diagnostic) -> Self {
2917        Self {
2918            message: message.into(),
2919            diagnostic: Some(Box::new(diagnostic)),
2920        }
2921    }
2922
2923    fn append_context(mut self, context: impl AsRef<str>) -> Self {
2924        self.message = format!("{}; {}", self.message, context.as_ref());
2925        self
2926    }
2927
2928    fn into_view_error(self) -> ScriptViewError {
2929        match self.diagnostic {
2930            Some(diagnostic) => ScriptViewError::ScriptDiagnostic {
2931                message: self.message,
2932                diagnostic,
2933            },
2934            None => ScriptViewError::Resume(self.message),
2935        }
2936    }
2937}
2938
2939impl From<String> for ScriptFailure {
2940    fn from(message: String) -> Self {
2941        Self::plain(message)
2942    }
2943}
2944
2945#[allow(clippy::struct_excessive_bools)]
2946struct ScriptHostView {
2947    view_id: String,
2948    window_id: String,
2949    engine: RuntimeEngine,
2950    lifecycle: ScriptLifecycle,
2951    primitives: PrimitiveRegistry,
2952    last_failure: Option<ScriptFailure>,
2953    theme: ThemeVariant,
2954    theme_handle: ThemeHandle,
2955    #[cfg(feature = "dev-reload")]
2956    development: bool,
2957    #[cfg(feature = "dev-reload")]
2958    inspector_open: bool,
2959    timings: Vec<crate::ExecutionTiming>,
2960    overlays: WindowOverlayCoordinator,
2961    host: ScriptViewHost,
2962    paint_background: bool,
2963    show_error_banner: bool,
2964    content_bounds: Option<Bounds<Pixels>>,
2965    factory: Rc<ScriptWindowFactory>,
2966    native_windows: Rc<RefCell<NativeWindowRegistry>>,
2967    host_focus: FocusHandle,
2968    focus_handles: BTreeMap<crate::NodeId, FocusHandle>,
2969    scroll_handles: BTreeMap<crate::NodeId, ScrollHandle>,
2970    scroll_anchors: BTreeMap<crate::NodeId, ScrollAnchor>,
2971    text_selection: crate::renderer::TextSelectionRegistry,
2972    last_motion_sample: Option<Instant>,
2973    state: Rc<Cell<ScriptViewState>>,
2974    activity_wake: crate::async_runtime::AsyncWake,
2975    _runtime_tasks: HostRuntimeTasks,
2976    #[cfg(feature = "dev-reload")]
2977    module_cache: ModuleCompileCache,
2978    #[cfg(feature = "dev-reload")]
2979    watcher: Option<FileWatcher>,
2980    #[cfg(feature = "dev-reload")]
2981    entry: PathBuf,
2982    #[cfg(feature = "dev-reload")]
2983    ui_root: PathBuf,
2984    #[cfg(feature = "dev-reload")]
2985    theme_path: PathBuf,
2986    #[cfg(feature = "dev-reload")]
2987    style_path: PathBuf,
2988    #[cfg(feature = "dev-reload")]
2989    _reload_task: Option<Task<()>>,
2990    #[cfg(feature = "dev-reload")]
2991    pending_reload_paths: BTreeSet<PathBuf>,
2992}
2993
2994fn nearest_scroll_ancestor(
2995    tree: &crate::RetainedUiTree,
2996    node: crate::NodeId,
2997) -> Option<crate::NodeId> {
2998    let mut current = tree.node(node)?.parent();
2999    while let Some(node) = current {
3000        let retained = tree.node(node)?;
3001        if retained.scrollable() {
3002            return Some(node);
3003        }
3004        current = retained.parent();
3005    }
3006    None
3007}
3008
3009struct ScriptRenderSnapshot {
3010    now: Instant,
3011    clock: crate::RuntimeClock,
3012    motion_preference: crate::MotionPreference,
3013    motion_quality: crate::MotionQuality,
3014    assets: AssetRegistry,
3015    theme: ThemeVariant,
3016    motions: BTreeMap<crate::MotionKey, f64>,
3017    motion_ghosts: Vec<crate::motion::MotionGhost>,
3018    signals: crate::SignalRegistry,
3019    geometry: crate::GeometryRegistry,
3020    pointer_capture: crate::PointerCaptureRegistry,
3021    virtual_requests: crate::VirtualRequestRegistry,
3022    direction: TextDirection,
3023    locale: String,
3024    number: Option<crate::NumberMetadata>,
3025}
3026
3027struct ScriptViewTransaction {
3028    runtime: crate::UiStateSnapshot,
3029    engine: crate::engine::RuntimeEngineCheckpoint,
3030    lifecycle: crate::lifecycle::ScriptLifecycleCheckpoint,
3031}
3032
3033fn handle_tab_navigation(event: &gpui::KeyDownEvent, window: &mut Window, cx: &mut App) {
3034    if event.keystroke.key.as_str() == "tab" {
3035        if event.keystroke.modifiers.shift {
3036            window.focus_prev(cx);
3037        } else {
3038            window.focus_next(cx);
3039        }
3040        cx.stop_propagation();
3041    }
3042}
3043
3044fn apply_root_text_direction(
3045    root: gpui::Stateful<gpui::Div>,
3046    direction: TextDirection,
3047) -> gpui::Stateful<gpui::Div> {
3048    match direction {
3049        TextDirection::LeftToRight => root.text_left(),
3050        TextDirection::RightToLeft => root.text_right(),
3051    }
3052}
3053
3054fn build_host_root(
3055    host_focus: &FocusHandle,
3056    theme: &ThemeVariant,
3057    paint_background: bool,
3058) -> gpui::Stateful<gpui::Div> {
3059    let root = div()
3060        .id("gpui-rhai-host")
3061        .size_full()
3062        .track_focus(host_focus)
3063        .text_color(rgba(theme.tokens.colors["text_primary"].as_rgba_hex()))
3064        .key_context(HOST_KEY_CONTEXT)
3065        .on_key_down(handle_tab_navigation);
3066    if paint_background {
3067        root.bg(rgba(theme.tokens.colors["surface"].as_rgba_hex()))
3068    } else {
3069        root
3070    }
3071}
3072
3073#[cfg(target_os = "macos")]
3074const fn error_banner_font_family() -> &'static str {
3075    "Menlo"
3076}
3077
3078#[cfg(target_os = "windows")]
3079const fn error_banner_font_family() -> &'static str {
3080    "Consolas"
3081}
3082
3083#[cfg(not(any(target_os = "macos", target_os = "windows")))]
3084const fn error_banner_font_family() -> &'static str {
3085    "DejaVu Sans Mono"
3086}
3087
3088fn build_error_banner(
3089    view_id: &str,
3090    window_id: &str,
3091    error: &str,
3092    theme: &ThemeVariant,
3093    selection: crate::renderer::TextSelectionRegistry,
3094    host_focus: FocusHandle,
3095    content: AnyElement,
3096) -> AnyElement {
3097    let selector = format!("gpui-rhai-error-banner:{view_id}");
3098    let owner = format!("window:{window_id}/view:{view_id}/error-banner");
3099    let error_text = crate::renderer::selectable_text_element(
3100        owner,
3101        error,
3102        theme.tokens.colors["selection"],
3103        selection,
3104        Some(host_focus),
3105    );
3106    div()
3107        .flex()
3108        .flex_col()
3109        .child(
3110            div()
3111                .debug_selector(move || selector.clone())
3112                .p_2()
3113                .font_family(error_banner_font_family())
3114                .text_size(px(12.0))
3115                .line_height(px(18.0))
3116                .bg(rgba(theme.tokens.colors["surface_raised"].as_rgba_hex()))
3117                .text_color(rgba(theme.tokens.colors["danger"].as_rgba_hex()))
3118                .border_1()
3119                .border_color(rgba(theme.tokens.colors["danger"].as_rgba_hex()))
3120                .child(error_text),
3121        )
3122        .child(content)
3123        .into_any_element()
3124}
3125
3126fn script_node_dispatcher(cx: &Context<ScriptHostView>) -> NodeEventDispatcher {
3127    let script_entity = cx.entity().downgrade();
3128    let native_entity = script_entity.clone();
3129    let signal_entity = script_entity.clone();
3130    let geometry_entity = script_entity.clone();
3131    NodeEventDispatcher::new(move |callback, payload, target, window, app| {
3132        script_entity
3133            .update(app, |view, cx| {
3134                view.handle_node_event(&callback, payload, target, window, cx)
3135            })
3136            .unwrap_or_else(|_| crate::EventResponse::new().stop())
3137    })
3138    .with_native(move |handler, event, payload, target, window, app| {
3139        native_entity
3140            .update(app, |view, cx| {
3141                view.handle_native_event(&handler, event, payload, target, window, cx)
3142            })
3143            .unwrap_or_else(|_| crate::EventResponse::new().stop())
3144    })
3145    .with_signal_write(move |signal, value, app| {
3146        let stale = crate::SignalError::Stale(signal.id().clone());
3147        signal_entity
3148            .update(app, |view, cx| {
3149                let changed = view.lifecycle.runtime().borrow_mut().signals.write_from(
3150                    &signal,
3151                    value,
3152                    crate::SignalWriter::Primitive,
3153                )?;
3154                if changed {
3155                    cx.notify();
3156                }
3157                Ok(changed)
3158            })
3159            .unwrap_or(Err(stale))
3160    })
3161    .with_element_bounds(move |reference, app| {
3162        geometry_entity
3163            .read_with(app, |view, _| {
3164                let runtime = view.lifecycle.runtime();
3165                let runtime = runtime.borrow();
3166                let node = runtime.element_refs.resolve(reference).ok()?;
3167                runtime
3168                    .geometry_for(Some(&view.view_id))
3169                    .get(node)
3170                    .map(|geometry| geometry.layout)
3171            })
3172            .ok()
3173            .flatten()
3174    })
3175}
3176
3177const fn system_appearance(appearance: WindowAppearance) -> SystemAppearance {
3178    match appearance {
3179        WindowAppearance::Dark | WindowAppearance::VibrantDark => SystemAppearance::Dark,
3180        WindowAppearance::Light | WindowAppearance::VibrantLight => SystemAppearance::Light,
3181    }
3182}
3183
3184fn resolve_root_theme(
3185    lifecycle: &ScriptLifecycle,
3186    fallback: &ThemeVariant,
3187    appearance: SystemAppearance,
3188) -> ThemeVariant {
3189    let runtime = lifecycle.runtime();
3190    resolve_root_theme_from_runtime(
3191        &runtime.borrow(),
3192        lifecycle.root_path(),
3193        lifecycle.window_id().unwrap_or_default(),
3194        appearance,
3195        fallback,
3196    )
3197}
3198
3199fn theme_handles_for_lifecycle(
3200    lifecycle: &ScriptLifecycle,
3201    fallback: &ThemeVariant,
3202    window: &Window,
3203    cx: &mut App,
3204) -> (ThemeHandle, ThemeHandle) {
3205    let handle = ThemeHandle::new(
3206        resolve_root_theme(lifecycle, fallback, system_appearance(window.appearance())),
3207        cx,
3208    );
3209    (handle.clone(), handle)
3210}
3211
3212fn attach_script_view_focus(
3213    host: &ScriptViewHost,
3214    view_id: &str,
3215    entity: &Entity<ScriptHostView>,
3216    cx: &App,
3217) {
3218    host.attach_view_focus(view_id, entity.read(cx).host_focus.clone());
3219}
3220
3221fn mounted_script_view_handle(
3222    entity: Entity<ScriptHostView>,
3223    theme: ThemeHandle,
3224    host: ScriptViewHost,
3225    config: ScriptViewConfig,
3226    state: Rc<Cell<ScriptViewState>>,
3227) -> ScriptViewHandle {
3228    ScriptViewHandle(Rc::new(ScriptViewHandleInner {
3229        entity,
3230        theme,
3231        host,
3232        view_id: config.view_id,
3233        state,
3234        measured_bounds: Rc::new(Cell::new(None)),
3235    }))
3236}
3237
3238fn mount_prepared_lifecycle(
3239    factory: &ScriptWindowFactory,
3240    engine: &mut RuntimeEngine,
3241    host: &ScriptViewHost,
3242    view_id: &str,
3243    window_id: &str,
3244) -> Result<ScriptLifecycle, ScriptViewError> {
3245    factory
3246        .mount_lifecycle(
3247            engine,
3248            factory.program(),
3249            view_id,
3250            window_id,
3251            host.window_policy(),
3252            true,
3253        )
3254        .inspect_err(|_| {
3255            host.unregister_view(view_id);
3256        })
3257}
3258
3259fn resolve_root_theme_from_runtime(
3260    runtime: &UiRuntimeState,
3261    root: &ComponentInstancePath,
3262    window_id: &str,
3263    appearance: SystemAppearance,
3264    fallback: &ThemeVariant,
3265) -> ThemeVariant {
3266    runtime
3267        .theme
3268        .as_ref()
3269        .and_then(|themes| themes.resolve(Some(window_id), Some(root), appearance).ok())
3270        .map_or_else(|| fallback.clone(), |resolved| resolved.variant().clone())
3271}
3272
3273impl Render for ScriptHostView {
3274    #[allow(clippy::too_many_lines)]
3275    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
3276        if self.state.get() != ScriptViewState::Active {
3277            return div().into_any_element();
3278        }
3279        self.prepare_host_render(window, cx);
3280        let motion_root = format!("window:{}/view:{}/root", self.window_id, self.view_id);
3281        let (motion_active, committed_motion_events) = self.sample_motion_frame(&motion_root);
3282        let dispatcher = script_node_dispatcher(cx);
3283        let appearance = system_appearance(window.appearance());
3284        let snapshot = self.render_snapshot(appearance);
3285        self.publish_theme_after_render(&snapshot.theme, cx);
3286        let a11y_active = window.is_a11y_active();
3287        let mut semantics = self.lifecycle.semantics().clone();
3288        if a11y_active {
3289            semantics.apply_primitive_projections(
3290                self.primitives
3291                    .accessibility_projections(self.lifecycle.retained(), cx),
3292            );
3293        }
3294        let render_resources = crate::renderer::WindowRenderResources {
3295            now: snapshot.now,
3296            clock: &snapshot.clock,
3297            motion_preference: snapshot.motion_preference,
3298            motion_quality: snapshot.motion_quality,
3299            assets: &snapshot.assets,
3300            dispatcher: &dispatcher,
3301            overlays: &self.overlays,
3302            motions: &snapshot.motions,
3303            signals: &snapshot.signals,
3304            geometry: &snapshot.geometry,
3305            pointer_capture: &snapshot.pointer_capture,
3306            focus_handles: &self.focus_handles,
3307            scroll_handles: &self.scroll_handles,
3308            scroll_anchors: &self.scroll_anchors,
3309            virtual_requests: &snapshot.virtual_requests,
3310            text_selection: &self.text_selection,
3311            host_focus: Some(&self.host_focus),
3312            direction: snapshot.direction,
3313            locale: &snapshot.locale,
3314            number: snapshot.number.as_ref(),
3315            ambient_text_color: None,
3316            root_path: &motion_root,
3317            view_id: &self.view_id,
3318            semantics: &semantics,
3319            a11y_active,
3320        };
3321        let content = self.lifecycle.retained().root().map_or_else(
3322            || div().child("Script view has no root").into_any_element(),
3323            |_| {
3324                GpuiNodeRenderer::render_retained_with_window_runtime(
3325                    self.lifecycle.retained(),
3326                    &snapshot.theme,
3327                    &InteractionState::default(),
3328                    &self.primitives,
3329                    &render_resources,
3330                )
3331            },
3332        );
3333        let content = match (&self.last_failure, self.show_error_banner) {
3334            (Some(failure), true) => build_error_banner(
3335                &self.view_id,
3336                &self.window_id,
3337                &failure.message,
3338                &snapshot.theme,
3339                self.text_selection.clone(),
3340                self.host_focus.clone(),
3341                content,
3342            ),
3343            _ => content,
3344        };
3345        let motion_ghosts = snapshot
3346            .motion_ghosts
3347            .iter()
3348            .map(|ghost| {
3349                crate::renderer::render_motion_ghost(
3350                    ghost,
3351                    &snapshot.theme,
3352                    &self.primitives,
3353                    &render_resources,
3354                )
3355            })
3356            .collect::<Vec<_>>();
3357        #[cfg(feature = "dev-reload")]
3358        let runtime = self.lifecycle.runtime();
3359        #[cfg(feature = "dev-reload")]
3360        let inspector = self.inspector_element(&runtime, &snapshot.theme);
3361        let text_selection = self.text_selection.clone();
3362        let root = build_host_root(&self.host_focus, &snapshot.theme, self.paint_background)
3363            .on_mouse_down(MouseButton::Left, move |event, window, _| {
3364                if text_selection.clear_outside(event.position) {
3365                    window.refresh();
3366                }
3367            })
3368            .child(content)
3369            .children(motion_ghosts)
3370            .children({
3371                #[cfg(feature = "dev-reload")]
3372                {
3373                    inspector
3374                }
3375                #[cfg(not(feature = "dev-reload"))]
3376                {
3377                    Option::<gpui::AnyElement>::None
3378                }
3379            });
3380        let root = apply_root_text_direction(root, snapshot.direction);
3381        let root = root
3382            .on_action(cx.listener(Self::dispatch_key_binding))
3383            .on_action(cx.listener(Self::copy_selected_text));
3384        #[cfg(feature = "dev-reload")]
3385        let root = root.on_action(cx.listener(Self::toggle_inspector));
3386        let has_committed_motion_events = !committed_motion_events.is_empty();
3387        cx.on_next_frame(window, move |view, _, cx| {
3388            view.lifecycle
3389                .runtime()
3390                .borrow()
3391                .geometry_for(Some(&view.view_id))
3392                .finish_frame();
3393            view.deliver_committed_motion_events(committed_motion_events, cx);
3394        });
3395        if motion_active || has_committed_motion_events {
3396            window.request_animation_frame();
3397        }
3398        crate::renderer::pointer_capture_router_element(
3399            root.into_any_element(),
3400            self.lifecycle.retained(),
3401            &dispatcher,
3402            &snapshot.pointer_capture,
3403            &snapshot.geometry,
3404            &self.scroll_handles,
3405        )
3406    }
3407}
3408
3409fn native_lifecycle_error(
3410    operation: &str,
3411    error: &crate::PrimitiveError,
3412    rollback: Option<&crate::PrimitiveError>,
3413) -> String {
3414    rollback.map_or_else(
3415        || format!("native primitive {operation} failed: {error}"),
3416        |rollback| {
3417            format!("native primitive {operation} failed: {error}; compensation failed: {rollback}")
3418        },
3419    )
3420}
3421
3422impl ScriptHostView {
3423    fn sample_motion_frame(&mut self, domain: &str) -> (bool, Vec<crate::MotionTimelineEvent>) {
3424        let runtime = self.lifecycle.runtime();
3425        let mut runtime = runtime.borrow_mut();
3426        let now = runtime.clock.now();
3427        let clock_advanced = self.last_motion_sample != Some(now);
3428        self.last_motion_sample = Some(now);
3429        let (frame, events) = sample_motion_domain(&mut runtime, domain, now);
3430        (clock_advanced && frame.needs_frame, events)
3431    }
3432
3433    fn deliver_committed_motion_events(
3434        &mut self,
3435        events: Vec<crate::MotionTimelineEvent>,
3436        cx: &mut Context<Self>,
3437    ) {
3438        match self.state.get() {
3439            ScriptViewState::Suspended => {
3440                self.lifecycle
3441                    .runtime()
3442                    .borrow_mut()
3443                    .motions
3444                    .prepend_timeline_events(events);
3445                return;
3446            }
3447            ScriptViewState::Disposed => return,
3448            ScriptViewState::Active => {}
3449        }
3450        let mut changed = false;
3451        let mut first_error = None;
3452        for event in events {
3453            if event.callback.is_none() {
3454                continue;
3455            }
3456            let result = self.run_script_transaction(|view| {
3457                let callback_changed = view.invoke_motion_timeline_callback(event)?;
3458                let mut work_changed = view.invoke_pending_effects()?;
3459                work_changed |= view
3460                    .lifecycle
3461                    .render_dirty(&mut view.engine)
3462                    .map_err(|error| view.lifecycle_failure(&error, None))?;
3463                Ok(callback_changed || work_changed)
3464            });
3465            match result {
3466                Ok(event_changed) => changed |= event_changed,
3467                Err(error) => {
3468                    first_error.get_or_insert(error);
3469                }
3470            }
3471        }
3472        if let Some(error) = first_error {
3473            self.set_failure(error);
3474            changed = true;
3475        }
3476        if changed {
3477            cx.notify();
3478        }
3479    }
3480
3481    fn lifecycle_failure(
3482        &self,
3483        error: &crate::LifecycleError,
3484        component: Option<&ComponentInstancePath>,
3485    ) -> ScriptFailure {
3486        self.lifecycle_failure_with_engine(&self.engine, error, component)
3487    }
3488
3489    fn lifecycle_failure_with_engine(
3490        &self,
3491        engine: &RuntimeEngine,
3492        error: &crate::LifecycleError,
3493        component: Option<&ComponentInstancePath>,
3494    ) -> ScriptFailure {
3495        let message = error.to_string();
3496        if let crate::LifecycleError::Runtime(runtime_error) = error
3497            && let Ok(context) = self.lifecycle.diagnostic_context(engine, component)
3498        {
3499            return ScriptFailure::with_diagnostic(
3500                message,
3501                Diagnostic::from_runtime(runtime_error, &context),
3502            );
3503        }
3504        ScriptFailure::plain(message)
3505    }
3506
3507    #[cfg(feature = "dev-reload")]
3508    fn runtime_failure_with_engine(
3509        &self,
3510        engine: &RuntimeEngine,
3511        error: &RuntimeError,
3512        component: Option<&ComponentInstancePath>,
3513    ) -> ScriptFailure {
3514        let message = error.to_string();
3515        match self.lifecycle.diagnostic_context(engine, component) {
3516            Ok(context) => {
3517                ScriptFailure::with_diagnostic(message, Diagnostic::from_runtime(error, &context))
3518            }
3519            Err(_) => ScriptFailure::plain(message),
3520        }
3521    }
3522
3523    fn set_failure(&mut self, failure: ScriptFailure) {
3524        self.last_failure = Some(failure);
3525    }
3526
3527    fn set_plain_failure(&mut self, message: impl Into<String>) {
3528        self.set_failure(ScriptFailure::plain(message));
3529    }
3530
3531    fn clear_failure(&mut self) {
3532        self.last_failure = None;
3533    }
3534
3535    fn failure_message(&self) -> Option<String> {
3536        self.last_failure
3537            .as_ref()
3538            .map(|failure| failure.message.clone())
3539    }
3540
3541    fn publish_theme_after_render(&self, theme: &ThemeVariant, cx: &mut Context<Self>) {
3542        if self.theme_handle.matches(theme, cx) {
3543            return;
3544        }
3545        let handle = self.theme_handle.clone();
3546        let theme = theme.clone();
3547        cx.defer(move |cx| handle.publish(theme, cx));
3548    }
3549
3550    fn execute_automation(
3551        &mut self,
3552        command: crate::AutomationCommand,
3553        window: &mut Window,
3554        cx: &mut Context<Self>,
3555    ) -> Result<crate::AutomationResult, ScriptViewError> {
3556        match command {
3557            crate::AutomationCommand::Dispatch {
3558                locator,
3559                event,
3560                payload,
3561            } => self.automation_dispatch(&locator, &event, payload, window, cx),
3562            crate::AutomationCommand::Action { id, payload } => {
3563                let action = ActionId::parse(&id)?;
3564                let invocation = self
3565                    .lifecycle
3566                    .runtime()
3567                    .borrow()
3568                    .actions
3569                    .dispatch(&action, payload.unwrap_or(UiValue::Null))?;
3570                let _ = self.handle_node_event(
3571                    &invocation.callback,
3572                    invocation.payload,
3573                    None,
3574                    window,
3575                    cx,
3576                );
3577                if let Some(error) = self.failure_message() {
3578                    return Err(crate::AutomationError::Command(error).into());
3579                }
3580                Ok(crate::AutomationResult::Action { id })
3581            }
3582            crate::AutomationCommand::AdvanceTime { millis } => {
3583                let duration = Duration::from_millis(millis);
3584                let clock = self.lifecycle.runtime().borrow().clock.clone();
3585                if !clock.advance(duration) {
3586                    return Err(crate::AutomationError::ClockNotControllable.into());
3587                }
3588                self.clear_failure();
3589                self.poll_async(cx);
3590                if let Some(error) = self.failure_message() {
3591                    return Err(crate::AutomationError::Command(error).into());
3592                }
3593                Ok(crate::AutomationResult::Advanced { millis })
3594            }
3595            crate::AutomationCommand::Snapshot | crate::AutomationCommand::Query { .. } => {
3596                unreachable!("read-only automation commands are handled by ScriptViewHandle")
3597            }
3598        }
3599    }
3600
3601    fn automation_dispatch(
3602        &mut self,
3603        locator: &crate::AutomationLocator,
3604        event: &str,
3605        payload: Option<UiValue>,
3606        window: &mut Window,
3607        cx: &mut Context<Self>,
3608    ) -> Result<crate::AutomationResult, ScriptViewError> {
3609        self.clear_failure();
3610        let runtime = self.lifecycle.runtime();
3611        let geometry = runtime.borrow().geometry_for(Some(&self.view_id));
3612        let mut accessibility = crate::AccessibilityTree::from_committed(
3613            self.lifecycle.semantics(),
3614            self.lifecycle.retained(),
3615            &geometry,
3616            true,
3617        );
3618        accessibility.apply_primitive_projections(
3619            self.primitives
3620                .accessibility_projections(self.lifecycle.retained(), cx),
3621        );
3622        let target = crate::automation::resolve_locator(&accessibility, locator)?;
3623        let target_node = self
3624            .lifecycle
3625            .retained()
3626            .node(target)
3627            .ok_or_else(|| crate::AutomationError::StaleTarget(target.get()))?;
3628        let payload = payload
3629            .or_else(|| target_node.handler_payload(event).cloned())
3630            .unwrap_or(UiValue::Null);
3631        let steps = crate::automation::dispatch_plan(self.lifecycle.retained(), target, event)?;
3632        let mut response = crate::EventResponse::new();
3633        let mut invoked = 0usize;
3634        let mut visited = Vec::new();
3635        let mut stop_after: Option<(crate::NodeId, crate::EventPhase)> = None;
3636        for step in steps {
3637            if stop_after.is_some_and(|phase| phase != (step.node, step.phase)) {
3638                break;
3639            }
3640            if visited.last() != Some(&step.node) {
3641                visited.push(step.node);
3642            }
3643            let event_target = geometry.get(step.node).map(|geometry| geometry.visual);
3644            let current = match &step.handler {
3645                crate::UiEventHandler::Script(callback) => {
3646                    self.handle_node_event(callback, payload.clone(), event_target, window, cx)
3647                }
3648                crate::UiEventHandler::Host(callback) => {
3649                    callback.invoke(payload.clone(), window, cx)
3650                }
3651                crate::UiEventHandler::Native(handler) => self.handle_native_event(
3652                    handler,
3653                    event.to_owned(),
3654                    payload.clone(),
3655                    event_target,
3656                    window,
3657                    cx,
3658                ),
3659            };
3660            invoked = invoked.saturating_add(1);
3661            if let Some(error) = self.failure_message() {
3662                return Err(crate::AutomationError::Command(error).into());
3663            }
3664            response.merge(current);
3665            match current.propagation() {
3666                crate::PropagationControl::StopImmediate => break,
3667                crate::PropagationControl::Stop => stop_after = Some((step.node, step.phase)),
3668                crate::PropagationControl::Continue => {}
3669            }
3670            if let Some(pointer_id) = automation_pointer_id(&payload) {
3671                match current.pointer_capture() {
3672                    crate::PointerCaptureDirective::Capture => {
3673                        runtime
3674                            .borrow()
3675                            .pointer_capture_for(Some(&self.view_id))
3676                            .capture(pointer_id, target);
3677                    }
3678                    crate::PointerCaptureDirective::Release => {
3679                        runtime
3680                            .borrow()
3681                            .pointer_capture_for(Some(&self.view_id))
3682                            .release(pointer_id);
3683                    }
3684                    crate::PointerCaptureDirective::None => {}
3685                }
3686            }
3687        }
3688        Ok(crate::AutomationResult::Dispatch {
3689            report: crate::AutomationDispatchReport {
3690                target: target.get(),
3691                visited: visited.into_iter().map(crate::NodeId::get).collect(),
3692                invoked,
3693                default_prevented: response.default_prevented(),
3694                stopped: response.stops_propagation(),
3695            },
3696        })
3697    }
3698
3699    fn render_snapshot(&self, appearance: SystemAppearance) -> ScriptRenderSnapshot {
3700        let runtime = self.lifecycle.runtime();
3701        let mut runtime = runtime.borrow_mut();
3702        let appearance_changed = runtime
3703            .window_appearances
3704            .insert(self.window_id.clone(), appearance)
3705            .is_some_and(|previous| previous != appearance);
3706        if appearance_changed {
3707            let invalidated = runtime
3708                .environment_dependencies
3709                .invalidate_theme_window(&self.window_id);
3710            runtime.mark_dirty(invalidated);
3711        }
3712        let root = self.lifecycle.root_path();
3713        let theme = resolve_root_theme_from_runtime(
3714            &runtime,
3715            root,
3716            &self.window_id,
3717            appearance,
3718            &self.theme,
3719        );
3720        let direction = runtime
3721            .locale
3722            .as_ref()
3723            .and_then(|locale| locale.direction(Some(&self.window_id), Some(root)).ok())
3724            .unwrap_or(TextDirection::LeftToRight);
3725        let locale = runtime
3726            .locale
3727            .as_ref()
3728            .and_then(|locale| locale.locale(Some(&self.window_id), Some(root)).ok())
3729            .unwrap_or("en")
3730            .to_owned();
3731        let number = runtime
3732            .locale
3733            .as_ref()
3734            .and_then(|locale| locale.number(Some(&self.window_id), Some(root)).ok())
3735            .cloned();
3736        let motion_domain = format!("window:{}/view:{}/root", self.window_id, self.view_id);
3737        ScriptRenderSnapshot {
3738            now: runtime.clock.now(),
3739            clock: runtime.clock.clone(),
3740            motion_preference: runtime.motions.preference(),
3741            motion_quality: runtime.motions.quality(),
3742            assets: runtime.assets.clone(),
3743            theme,
3744            motions: runtime.motion_values.clone(),
3745            motion_ghosts: runtime
3746                .motion_ghosts
3747                .iter()
3748                .filter(|ghost| ghost.domain == motion_domain)
3749                .cloned()
3750                .collect(),
3751            signals: runtime.signals.clone(),
3752            geometry: runtime.geometry_for(Some(&self.view_id)),
3753            pointer_capture: runtime.pointer_capture_for(Some(&self.view_id)),
3754            virtual_requests: runtime.virtual_requests.clone(),
3755            direction,
3756            locale,
3757            number,
3758        }
3759    }
3760
3761    fn prepare_host_render(&mut self, window: &mut Window, cx: &mut Context<Self>) {
3762        self.prepare_render(window);
3763        self.lifecycle
3764            .runtime()
3765            .borrow()
3766            .geometry_for(Some(&self.view_id))
3767            .begin_frame();
3768        self.text_selection.retain(self.lifecycle.retained());
3769        self.sync_focus_handles(cx);
3770        self.process_element_commands(window, cx);
3771    }
3772
3773    fn set_content_bounds(&mut self, bounds: Bounds<Pixels>, cx: &mut Context<Self>) {
3774        if self.content_bounds != Some(bounds) {
3775            self.content_bounds = Some(bounds);
3776            cx.notify();
3777        }
3778    }
3779
3780    fn prepare_render(&mut self, window: &Window) {
3781        if !self.host.frame_active() {
3782            self.set_plain_failure(
3783                "embedded ScriptView must be rendered inside ScriptViewHost::container",
3784            );
3785        }
3786        self.sync_viewport_class(window);
3787        debug_assert!(self.engine.is_current(self.lifecycle.generation()));
3788        self.reconcile_primitive_lifecycle();
3789    }
3790
3791    fn sync_focus_handles(&mut self, cx: &mut Context<Self>) {
3792        let active = self
3793            .lifecycle
3794            .retained()
3795            .nodes()
3796            .filter(|node| node.element_ref().is_some())
3797            .map(crate::RetainedNode::id)
3798            .collect::<BTreeSet<_>>();
3799        self.focus_handles.retain(|node, _| active.contains(node));
3800        for node in active {
3801            self.focus_handles
3802                .entry(node)
3803                .or_insert_with(|| cx.focus_handle());
3804        }
3805        let scrollable = self
3806            .lifecycle
3807            .retained()
3808            .nodes()
3809            .filter(|node| node.scrollable())
3810            .map(crate::RetainedNode::id)
3811            .collect::<BTreeSet<_>>();
3812        self.scroll_handles
3813            .retain(|node, _| scrollable.contains(node));
3814        for node in scrollable {
3815            self.scroll_handles.entry(node).or_default();
3816        }
3817        let anchors = self
3818            .lifecycle
3819            .retained()
3820            .nodes()
3821            .filter(|node| node.element_ref().is_some())
3822            .filter_map(|node| {
3823                nearest_scroll_ancestor(self.lifecycle.retained(), node.id())
3824                    .map(|ancestor| (node.id(), ancestor))
3825            })
3826            .collect::<BTreeMap<_, _>>();
3827        self.scroll_anchors
3828            .retain(|node, _| anchors.contains_key(node));
3829        for (node, ancestor) in anchors {
3830            if let Some(handle) = self.scroll_handles.get(&ancestor) {
3831                self.scroll_anchors
3832                    .entry(node)
3833                    .or_insert_with(|| ScrollAnchor::for_handle(handle.clone()));
3834            }
3835        }
3836    }
3837
3838    fn process_element_commands(&mut self, window: &mut Window, cx: &mut Context<Self>) {
3839        let commands = self
3840            .lifecycle
3841            .runtime()
3842            .borrow_mut()
3843            .take_window_element_commands(&self.window_id);
3844        for command in commands {
3845            match command {
3846                crate::element_ref::ElementCommand::Focus { node, .. } => {
3847                    if let Some(handle) = self.focus_handles.get(&node) {
3848                        handle.focus(window, cx);
3849                    } else {
3850                        self.set_plain_failure(format!(
3851                            "retained node {node} is not focusable or has been unmounted"
3852                        ));
3853                    }
3854                }
3855                crate::element_ref::ElementCommand::ScrollTo { node, x, y, .. } => {
3856                    if let Some(handle) = self.scroll_handles.get(&node) {
3857                        handle.set_offset(gpui::point(pixel_from_f64(-x), pixel_from_f64(-y)));
3858                    } else {
3859                        self.set_plain_failure(format!(
3860                            "retained node {node} is not a scroll container or has been unmounted"
3861                        ));
3862                    }
3863                }
3864                crate::element_ref::ElementCommand::ScrollIntoView { node, .. } => {
3865                    if let Some(anchor) = self.scroll_anchors.get(&node) {
3866                        anchor.scroll_to(window, cx);
3867                    } else {
3868                        self.set_plain_failure(format!(
3869                            "retained node {node} has no scrollable ancestor or was unmounted"
3870                        ));
3871                    }
3872                }
3873            }
3874        }
3875    }
3876
3877    fn reconcile_primitive_lifecycle(&mut self) {
3878        if !self.lifecycle.retained().is_empty()
3879            && let Err(error) = self.primitives.retain_tree(self.lifecycle.retained())
3880        {
3881            self.set_plain_failure(error.to_string());
3882        }
3883    }
3884
3885    fn sync_viewport_class(&mut self, window: &Window) {
3886        let width = self.content_bounds.map_or_else(
3887            || f64::from(window.viewport_size().width),
3888            |bounds| f64::from(bounds.size.width),
3889        );
3890        let needs_update = self
3891            .lifecycle
3892            .runtime()
3893            .borrow()
3894            .responsive
3895            .would_update_window(&self.window_id, width);
3896        match needs_update {
3897            Ok(false) => return,
3898            Err(error) => {
3899                self.set_plain_failure(error.to_string());
3900                return;
3901            }
3902            Ok(true) => {}
3903        }
3904        let result = self.run_script_transaction(|view| {
3905            let runtime = view.lifecycle.runtime();
3906            let changed = {
3907                let mut runtime = runtime.borrow_mut();
3908                let changed = runtime
3909                    .responsive
3910                    .update_window(&view.window_id, width)
3911                    .map_err(|error| error.to_string())?;
3912                if changed {
3913                    let invalidated = runtime
3914                        .environment_dependencies
3915                        .invalidate_viewport(&view.window_id);
3916                    runtime.mark_dirty(invalidated);
3917                }
3918                changed
3919            };
3920            if changed {
3921                let result = view.lifecycle.render_dirty(&mut view.engine);
3922                result.map_err(|error| view.lifecycle_failure(&error, None))?;
3923            }
3924            Ok(())
3925        });
3926        if let Err(error) = result {
3927            self.set_failure(error);
3928        }
3929    }
3930
3931    fn dispatch_key_binding(
3932        &mut self,
3933        action: &DispatchScriptAction,
3934        window: &mut Window,
3935        cx: &mut Context<Self>,
3936    ) {
3937        let result = ActionId::parse(&action.id).and_then(|id| {
3938            self.lifecycle
3939                .runtime()
3940                .borrow()
3941                .actions
3942                .dispatch(&id, UiValue::Null)
3943        });
3944        match result {
3945            Ok(invocation) => {
3946                if let Ok(mut runtime) = self.lifecycle.runtime().try_borrow_mut() {
3947                    runtime.traces.push(
3948                        crate::RuntimeTraceKind::Action,
3949                        self.lifecycle.root_path().to_string(),
3950                        format!("key binding {}", action.id),
3951                        None,
3952                        false,
3953                    );
3954                }
3955                self.handle_node_event(&invocation.callback, invocation.payload, None, window, cx);
3956            }
3957            Err(error) => {
3958                self.set_plain_failure(error.to_string());
3959                cx.notify();
3960            }
3961        }
3962    }
3963
3964    fn copy_selected_text(&mut self, _: &CopySelectedText, _: &mut Window, cx: &mut Context<Self>) {
3965        if let Some(text) = self.text_selection.selected_text() {
3966            cx.write_to_clipboard(gpui::ClipboardItem::new_string(text));
3967            cx.stop_propagation();
3968        }
3969    }
3970
3971    fn invoke_pending_effects(&mut self) -> Result<bool, ScriptFailure> {
3972        let mut processed = 0usize;
3973        loop {
3974            let (actions, events) = {
3975                let runtime = self.lifecycle.runtime();
3976                let mut runtime = runtime.borrow_mut();
3977                (
3978                    runtime.drain_pending_actions(),
3979                    runtime.drain_pending_events(),
3980                )
3981            };
3982            if actions.is_empty() && events.is_empty() {
3983                return Ok(processed > 0);
3984            }
3985            processed = processed.saturating_add(actions.len() + events.len());
3986            if processed > 64 {
3987                return Err(ScriptFailure::plain(
3988                    "semantic event/action dispatch exceeded the 64-callback budget",
3989                ));
3990            }
3991            for action in actions {
3992                let component = action.callback.component().cloned();
3993                let result = self.lifecycle.invoke_callback_transactional(
3994                    &self.engine,
3995                    &action.callback,
3996                    action.payload,
3997                );
3998                let _ =
3999                    result.map_err(|error| self.lifecycle_failure(&error, component.as_ref()))?;
4000            }
4001            for event in events {
4002                let component = event.target.clone();
4003                let result = self
4004                    .lifecycle
4005                    .invoke_component_event_transactional(&self.engine, event);
4006                let _ = result.map_err(|error| self.lifecycle_failure(&error, Some(&component)))?;
4007            }
4008        }
4009    }
4010
4011    fn invoke_motion_timeline_callback(
4012        &mut self,
4013        event: crate::MotionTimelineEvent,
4014    ) -> Result<bool, ScriptFailure> {
4015        let Some(callback) = event.callback else {
4016            return Ok(false);
4017        };
4018        if callback.generation() != self.lifecycle.generation()
4019            || callback
4020                .component()
4021                .zip(callback.incarnation())
4022                .is_some_and(|(component, incarnation)| {
4023                    self.lifecycle
4024                        .runtime()
4025                        .borrow()
4026                        .component_incarnation(component)
4027                        != Some(incarnation)
4028                })
4029        {
4030            return Ok(false);
4031        }
4032        let component = callback.component().cloned();
4033        let payload = UiValue::Map(BTreeMap::from([
4034            (
4035                "name".to_owned(),
4036                UiValue::String(event.handle.name().to_owned()),
4037            ),
4038            (
4039                "kind".to_owned(),
4040                UiValue::String(
4041                    match event.kind {
4042                        crate::MotionTimelineEventKind::Complete => "complete",
4043                        crate::MotionTimelineEventKind::Cancel => "cancel",
4044                    }
4045                    .to_owned(),
4046                ),
4047            ),
4048        ]));
4049        let _ = self
4050            .lifecycle
4051            .invoke_callback(&self.engine, &callback, payload)
4052            .map_err(|error| self.lifecycle_failure(&error, component.as_ref()))?;
4053        Ok(true)
4054    }
4055
4056    #[cfg(feature = "dev-reload")]
4057    fn inspector_element(
4058        &self,
4059        runtime: &Rc<RefCell<UiRuntimeState>>,
4060        theme: &ThemeVariant,
4061    ) -> Option<gpui::AnyElement> {
4062        (self.development && self.inspector_open).then(|| {
4063            let components = self.engine.component_exports().unwrap_or_default();
4064            let runtime = runtime.borrow();
4065            let snapshot = crate::InspectorSnapshot::capture_for_view(
4066                self.lifecycle.root(),
4067                &runtime,
4068                theme,
4069                &components,
4070                self.timings.clone(),
4071                Some(&self.view_id),
4072            );
4073            crate::devtools::inspector_element(&snapshot)
4074        })
4075    }
4076
4077    fn should_close(&mut self, cx: &mut Context<Self>) -> bool {
4078        if self.state.get() == ScriptViewState::Disposed {
4079            return true;
4080        }
4081        if self.state.get() == ScriptViewState::Suspended {
4082            self.release_view();
4083            return true;
4084        }
4085        let forced = self
4086            .native_windows
4087            .borrow_mut()
4088            .force_close
4089            .remove(&self.window_id);
4090        if forced {
4091            self.release_view();
4092            return true;
4093        }
4094        let handler = self
4095            .lifecycle
4096            .runtime()
4097            .borrow()
4098            .windows
4099            .close_handler(&self.window_id);
4100        let Some(handler) = handler else {
4101            self.release_view();
4102            return true;
4103        };
4104        let component = handler.component().cloned();
4105        let result = self.run_script_transaction(|view| {
4106            let result = view
4107                .lifecycle
4108                .invoke_callback(&view.engine, &handler, UiValue::Null);
4109            let _ = result.map_err(|error| view.lifecycle_failure(&error, component.as_ref()))?;
4110            view.invoke_pending_effects()?;
4111            let result = view.lifecycle.render_dirty(&mut view.engine);
4112            result.map_err(|error| view.lifecycle_failure(&error, None))?;
4113            Ok(())
4114        });
4115        match result {
4116            Ok(()) => {
4117                self.clear_failure();
4118                self.process_window_commands(cx);
4119                cx.notify();
4120                false
4121            }
4122            Err(error) => {
4123                self.set_failure(error);
4124                self.release_view();
4125                true
4126            }
4127        }
4128    }
4129
4130    fn suspend_view(
4131        &mut self,
4132        window: &mut Window,
4133        cx: &mut Context<Self>,
4134    ) -> Result<bool, ScriptViewError> {
4135        if let Err(error) = self.primitives.suspend_mounted(cx) {
4136            let rollback = self.restore_native_active(cx).err();
4137            let message = native_lifecycle_error("suspend", &error, rollback.as_ref());
4138            if rollback.is_some() {
4139                self.host
4140                    .quiesce_view(&self.view_id, &self.host_focus, window, cx);
4141                let message = self.fault_native_lifecycle(message, cx);
4142                return Err(ScriptViewError::Suspend(message));
4143            }
4144            self.set_plain_failure(message.clone());
4145            return Err(ScriptViewError::Suspend(message));
4146        }
4147        let changed = match self.lifecycle.suspend(&mut self.engine) {
4148            Ok(changed) => changed,
4149            Err(error) => {
4150                let rollback = self.restore_native_active(cx).err();
4151                let rollback_failed = rollback.is_some();
4152                let message = rollback.as_ref().map_or_else(
4153                    || error.to_string(),
4154                    |rollback| format!("{error}; native rollback failed: {rollback}"),
4155                );
4156                if rollback_failed {
4157                    self.host
4158                        .quiesce_view(&self.view_id, &self.host_focus, window, cx);
4159                    let message = self.fault_native_lifecycle(message, cx);
4160                    return Err(ScriptViewError::Suspend(message));
4161                }
4162                self.set_plain_failure(message);
4163                return Err(error.into());
4164            }
4165        };
4166        if !changed {
4167            if let Err(error) = self.restore_native_active(cx) {
4168                let message = self.fault_native_lifecycle(error.to_string(), cx);
4169                return Err(ScriptViewError::Suspend(message));
4170            }
4171            return Ok(false);
4172        }
4173        self.host
4174            .quiesce_view(&self.view_id, &self.host_focus, window, cx);
4175        self.state.set(ScriptViewState::Suspended);
4176        self.clear_failure();
4177        cx.notify();
4178        Ok(true)
4179    }
4180
4181    #[allow(clippy::too_many_lines)]
4182    fn resume_view(&mut self, cx: &mut Context<Self>) -> Result<bool, ScriptViewError> {
4183        if self.state.get() == ScriptViewState::Active {
4184            return Ok(false);
4185        }
4186        #[cfg(feature = "dev-reload")]
4187        let pending_reload_error = self.apply_pending_reload_on_resume();
4188        #[cfg(not(feature = "dev-reload"))]
4189        let pending_reload_error: Option<ScriptFailure> = None;
4190        self.collect_suspended_deliveries()?;
4191        let program = self.factory.program();
4192        let migrating = program.compiled.generation() != self.lifecycle.generation();
4193        let previous_exports = migrating
4194            .then(|| self.engine.component_exports())
4195            .transpose()
4196            .map_err(|error| ScriptViewError::Resume(error.to_string()))?;
4197        let previous_renderers = migrating
4198            .then(|| self.engine.component_renderer_snapshot())
4199            .transpose()
4200            .map_err(|error| ScriptViewError::Resume(error.to_string()))?;
4201        if migrating {
4202            self.engine
4203                .restore_component_exports(program.component_exports.clone())
4204                .map_err(|error| ScriptViewError::Resume(error.to_string()))?;
4205            if let Err(error) = self
4206                .engine
4207                .restore_component_renderers(program.component_renderers.clone())
4208            {
4209                if let Some(previous_exports) = previous_exports.clone() {
4210                    let _ = self.engine.restore_component_exports(previous_exports);
4211                }
4212                return Err(ScriptViewError::Resume(error.to_string()));
4213            }
4214        }
4215        self.prepare_native_resume(cx)?;
4216        let result = self.run_script_transaction(|view| {
4217            if migrating {
4218                view.lifecycle
4219                    .runtime()
4220                    .borrow_mut()
4221                    .discard_component_async_before_generation(
4222                        view.lifecycle.root_path(),
4223                        program.compiled.generation(),
4224                    );
4225                let result = view.lifecycle.resume_reload(
4226                    &mut view.engine,
4227                    program.compiled,
4228                    &program.state_schema,
4229                );
4230                result
4231                    .map(|_| true)
4232                    .map_err(|error| view.lifecycle_failure(&error, None))?;
4233                return Ok(true);
4234            }
4235            let pending = view
4236                .lifecycle
4237                .runtime()
4238                .borrow_mut()
4239                .take_window_async(&view.window_id, view.lifecycle.root_path());
4240            for delivery in pending {
4241                let component = delivery
4242                    .callback
4243                    .component()
4244                    .cloned()
4245                    .or_else(|| delivery.scope.component().cloned());
4246                let result = view.lifecycle.invoke_async_delivery(&view.engine, delivery);
4247                let _ =
4248                    result.map_err(|error| view.lifecycle_failure(&error, component.as_ref()))?;
4249            }
4250            view.invoke_pending_effects()?;
4251            let result = view.lifecycle.resume(&mut view.engine);
4252            result.map_err(|error| view.lifecycle_failure(&error, None))
4253        });
4254        match result {
4255            Ok(changed) => {
4256                if let Err(error) = self.primitives.commit_resume_mounted(cx) {
4257                    let rollback = self.primitives.suspend_mounted(cx).err();
4258                    let message =
4259                        native_lifecycle_error("commit resume", &error, rollback.as_ref());
4260                    let message = self.fault_native_lifecycle(message, cx);
4261                    return Err(ScriptViewError::Resume(message));
4262                }
4263                self.state.set(ScriptViewState::Active);
4264                self.activity_wake.notify();
4265                if let Some(error) = pending_reload_error {
4266                    self.set_failure(error);
4267                } else {
4268                    self.clear_failure();
4269                }
4270                self.collect_timings();
4271                cx.notify();
4272                Ok(changed)
4273            }
4274            Err(error) => {
4275                let native_rollback = self.primitives.suspend_mounted(cx).err();
4276                if let Some(previous_exports) = previous_exports {
4277                    let _ = self.engine.restore_component_exports(previous_exports);
4278                }
4279                if let Some(previous_renderers) = previous_renderers {
4280                    let _ = self.engine.restore_component_renderers(previous_renderers);
4281                }
4282                let public_error = error.clone().into_view_error();
4283                if let Some(native_rollback) = native_rollback {
4284                    let message = format!(
4285                        "{public_error}; native suspend compensation failed: {native_rollback}"
4286                    );
4287                    let message = self.fault_native_lifecycle(message, cx);
4288                    Err(ScriptViewError::Resume(message))
4289                } else {
4290                    self.set_failure(error);
4291                    Err(public_error)
4292                }
4293            }
4294        }
4295    }
4296
4297    fn prepare_native_resume(&mut self, cx: &mut Context<Self>) -> Result<(), ScriptViewError> {
4298        if let Err(error) = self.primitives.resume_mounted(cx) {
4299            let rollback = self.primitives.suspend_mounted(cx).err();
4300            let message = native_lifecycle_error("resume", &error, rollback.as_ref());
4301            if rollback.is_some() {
4302                let message = self.fault_native_lifecycle(message, cx);
4303                return Err(ScriptViewError::Resume(message));
4304            }
4305            self.set_plain_failure(message.clone());
4306            return Err(ScriptViewError::Resume(message));
4307        }
4308        Ok(())
4309    }
4310
4311    fn restore_native_active(
4312        &mut self,
4313        cx: &mut Context<Self>,
4314    ) -> Result<(), crate::PrimitiveError> {
4315        self.primitives.resume_mounted(cx)?;
4316        self.primitives.commit_resume_mounted(cx)
4317    }
4318
4319    fn fault_native_lifecycle(&mut self, message: String, cx: &mut Context<Self>) -> String {
4320        self.set_plain_failure(message.clone());
4321        self.release_view();
4322        cx.notify();
4323        message
4324    }
4325
4326    fn collect_suspended_deliveries(&mut self) -> Result<(), ScriptViewError> {
4327        let generation = self.lifecycle.generation();
4328        let runtime = self.lifecycle.runtime();
4329        let mut runtime = runtime.borrow_mut();
4330        let capacity = runtime.suspended_delivery_capacity_remaining();
4331        let mut deliveries = runtime.tasks.drain_up_to(generation, capacity);
4332        let remaining = capacity.saturating_sub(deliveries.len());
4333        deliveries.extend(runtime.subscriptions.drain_up_to(generation, remaining));
4334        runtime.trace_subscription_closures();
4335        runtime.queue_suspended_async(deliveries)?;
4336        Ok(())
4337    }
4338
4339    fn release_view(&mut self) {
4340        if self.state.get() == ScriptViewState::Disposed {
4341            return;
4342        }
4343        if let Err(error) = self.primitives.retain_mounted(&BTreeSet::new()) {
4344            self.set_plain_failure(error.to_string());
4345        }
4346        let _ = self.lifecycle.dispose(&mut self.engine);
4347        let root = self.lifecycle.root_path().clone();
4348        let _ = self
4349            .lifecycle
4350            .runtime()
4351            .borrow_mut()
4352            .release_window(&self.window_id, &root);
4353        self.lifecycle
4354            .runtime()
4355            .borrow_mut()
4356            .remove_presentation(&self.view_id);
4357        let mut native = self.native_windows.borrow_mut();
4358        native.handles.remove(&self.window_id);
4359        native.force_close.remove(&self.window_id);
4360        drop(native);
4361        self.host.unregister_view(&self.view_id);
4362        self.state.set(ScriptViewState::Disposed);
4363        self.activity_wake.notify();
4364    }
4365
4366    fn process_window_commands(&mut self, cx: &mut Context<Self>) {
4367        let commands = self
4368            .lifecycle
4369            .runtime()
4370            .borrow_mut()
4371            .windows
4372            .drain_commands();
4373        for command in commands {
4374            let result = match command {
4375                WindowCommand::Open(spec) => {
4376                    open_secondary_window(&spec, &self.factory, &self.native_windows, cx)
4377                }
4378                WindowCommand::Focus(id) => {
4379                    let handle = self.native_windows.borrow().handles.get(&id).copied();
4380                    handle.map_or_else(
4381                        || {
4382                            Err(ScriptFailure::plain(format!(
4383                                "native window `{id}` is unavailable"
4384                            )))
4385                        },
4386                        |handle| {
4387                            handle
4388                                .update(cx, |_, window, _| window.activate_window())
4389                                .map_err(|error| ScriptFailure::plain(error.to_string()))
4390                        },
4391                    )
4392                }
4393                WindowCommand::Close(id) => {
4394                    let handle = {
4395                        let mut native = self.native_windows.borrow_mut();
4396                        native.force_close.insert(id.clone());
4397                        native.handles.get(&id).copied()
4398                    };
4399                    handle.map_or_else(
4400                        || {
4401                            self.native_windows.borrow_mut().force_close.remove(&id);
4402                            Err(ScriptFailure::plain(format!(
4403                                "native window `{id}` is unavailable"
4404                            )))
4405                        },
4406                        |handle| {
4407                            // A script commonly confirms closing from an event
4408                            // dispatched by the same window. Updating that window
4409                            // recursively fails in GPUI with `window not found`, so
4410                            // remove it after the current entity update unwinds.
4411                            let view = cx.weak_entity();
4412                            let native_windows = Rc::clone(&self.native_windows);
4413                            let close_id = id.clone();
4414                            cx.defer(move |cx| {
4415                                if let Err(error) =
4416                                    handle.update(cx, |_, window, _| window.remove_window())
4417                                {
4418                                    native_windows.borrow_mut().force_close.remove(&close_id);
4419                                    let message = error.to_string();
4420                                    let _ = view.update(cx, |view, cx| {
4421                                        view.set_plain_failure(message);
4422                                        cx.notify();
4423                                    });
4424                                }
4425                            });
4426                            Ok(())
4427                        },
4428                    )
4429                }
4430            };
4431            if let Err(error) = result {
4432                self.set_failure(error);
4433            }
4434        }
4435    }
4436
4437    #[cfg(feature = "dev-reload")]
4438    fn toggle_inspector(&mut self, _: &ToggleInspector, _: &mut Window, cx: &mut Context<Self>) {
4439        if self.development {
4440            self.inspector_open = !self.inspector_open;
4441            cx.notify();
4442        }
4443    }
4444
4445    fn handle_node_event(
4446        &mut self,
4447        callback: &ScriptCallback,
4448        payload: UiValue,
4449        event_target: Option<crate::GeometryBounds>,
4450        window: &mut Window,
4451        cx: &mut Context<Self>,
4452    ) -> crate::EventResponse {
4453        if self.state.get() != ScriptViewState::Active {
4454            return crate::EventResponse::new().stop();
4455        }
4456        if let Ok(mut runtime) = self.lifecycle.runtime().try_borrow_mut() {
4457            runtime.traces.push(
4458                crate::RuntimeTraceKind::Event,
4459                "/App[root]",
4460                format!("callback {}", callback.name()),
4461                None,
4462                true,
4463            );
4464        }
4465        let callback_result = self.run_script_transaction(|view| {
4466            let result = view.lifecycle.invoke_callback_with_event_target(
4467                &view.engine,
4468                callback,
4469                payload,
4470                event_target,
4471            );
4472            let value =
4473                result.map_err(|error| view.lifecycle_failure(&error, callback.component()))?;
4474            view.invoke_pending_effects()?;
4475            let result = view.lifecycle.render_dirty(&mut view.engine);
4476            result.map_err(|error| view.lifecycle_failure(&error, None))?;
4477            Ok(value)
4478        });
4479        let response = callback_result.as_ref().map_or_else(
4480            |_| crate::EventResponse::new().stop(),
4481            event_response_from_dynamic,
4482        );
4483        let succeeded = callback_result.is_ok();
4484        match callback_result {
4485            Ok(_) => self.clear_failure(),
4486            Err(error) => self.set_failure(error),
4487        }
4488        self.process_window_commands(cx);
4489        self.process_element_commands(window, cx);
4490        self.collect_timings();
4491        cx.notify();
4492        if succeeded {
4493            response
4494        } else {
4495            crate::EventResponse::new().stop()
4496        }
4497    }
4498
4499    fn handle_native_event(
4500        &mut self,
4501        handler: &crate::NativeHandlerRef,
4502        event: String,
4503        payload: UiValue,
4504        event_target: Option<crate::GeometryBounds>,
4505        window: &mut Window,
4506        cx: &mut Context<Self>,
4507    ) -> crate::EventResponse {
4508        if self.state.get() != ScriptViewState::Active {
4509            return crate::EventResponse::new().stop();
4510        }
4511        let registry = self.engine.native_handler_registry();
4512        let result = self.run_script_transaction(|view| {
4513            let response = {
4514                let runtime = view.lifecycle.runtime();
4515                let mut runtime = runtime
4516                    .try_borrow_mut()
4517                    .map_err(|_| "UI runtime state is already borrowed".to_owned())?;
4518                registry
4519                    .invoke(
4520                        handler,
4521                        crate::NativeEvent {
4522                            name: event,
4523                            payload,
4524                            target: event_target,
4525                        },
4526                        &mut runtime,
4527                        window,
4528                        cx,
4529                    )
4530                    .map_err(|error| error.to_string())?
4531            };
4532            view.invoke_pending_effects()?;
4533            let result = view.lifecycle.render_dirty(&mut view.engine);
4534            result.map_err(|error| view.lifecycle_failure(&error, None))?;
4535            Ok(response)
4536        });
4537        match result {
4538            Ok(response) => {
4539                self.clear_failure();
4540                self.process_window_commands(cx);
4541                self.process_element_commands(window, cx);
4542                self.collect_timings();
4543                cx.notify();
4544                response
4545            }
4546            Err(error) => {
4547                self.set_failure(error);
4548                cx.notify();
4549                crate::EventResponse::new().stop()
4550            }
4551        }
4552    }
4553
4554    fn begin_script_transaction(&self) -> Result<ScriptViewTransaction, String> {
4555        let runtime = self
4556            .lifecycle
4557            .runtime()
4558            .try_borrow_mut()
4559            .map_err(|_| "UI runtime state is already borrowed".to_owned())?
4560            .begin_transaction()
4561            .map_err(|error| error.to_string())?;
4562        Ok(ScriptViewTransaction {
4563            runtime,
4564            engine: self.engine.execution_checkpoint(),
4565            lifecycle: self.lifecycle.execution_checkpoint(),
4566        })
4567    }
4568
4569    fn rollback_script_transaction(
4570        &mut self,
4571        transaction: ScriptViewTransaction,
4572    ) -> Result<(), String> {
4573        let runtime = self.lifecycle.runtime();
4574        let runtime_result = match runtime.try_borrow_mut() {
4575            Ok(mut runtime) => runtime
4576                .restore(transaction.runtime)
4577                .map_err(|error| error.to_string()),
4578            Err(_) => Err("UI runtime state is already borrowed".to_owned()),
4579        };
4580        self.engine.restore_execution_checkpoint(transaction.engine);
4581        self.lifecycle
4582            .restore_execution_checkpoint(transaction.lifecycle);
4583        runtime_result
4584    }
4585
4586    fn run_script_transaction<T>(
4587        &mut self,
4588        operation: impl FnOnce(&mut Self) -> Result<T, ScriptFailure>,
4589    ) -> Result<T, ScriptFailure> {
4590        let transaction = self
4591            .begin_script_transaction()
4592            .map_err(ScriptFailure::from)?;
4593        match operation(self) {
4594            Ok(value) => {
4595                let runtime = self.lifecycle.runtime();
4596                let commit = match runtime.try_borrow_mut() {
4597                    Ok(mut runtime) => runtime
4598                        .commit_transaction()
4599                        .map_err(|error| error.to_string()),
4600                    Err(_) => Err("UI runtime state is already borrowed".to_owned()),
4601                };
4602                match commit {
4603                    Ok(()) => Ok(value),
4604                    Err(error) => {
4605                        let rollback = self.rollback_script_transaction(transaction);
4606                        Err(ScriptFailure::plain(rollback.map_or_else(
4607                            |rollback| {
4608                                format!(
4609                                    "transaction commit failed: {error}; rollback also failed: {rollback}"
4610                                )
4611                            },
4612                            |()| format!("transaction commit failed: {error}"),
4613                        )))
4614                    }
4615                }
4616            }
4617            Err(error) => {
4618                let rollback = self.rollback_script_transaction(transaction);
4619                match rollback {
4620                    Ok(()) => Err(error),
4621                    Err(rollback) => Err(error
4622                        .append_context(format!("transaction rollback also failed: {rollback}"))),
4623                }
4624            }
4625        }
4626    }
4627
4628    fn collect_timings(&mut self) {
4629        self.timings.extend(self.engine.take_timings());
4630        if self.timings.len() > 200 {
4631            self.timings.drain(0..self.timings.len() - 200);
4632        }
4633    }
4634
4635    #[allow(clippy::too_many_lines)]
4636    fn poll_async(&mut self, cx: &mut Context<Self>) {
4637        if self.state.get() == ScriptViewState::Suspended {
4638            if let Err(error) = self.collect_suspended_deliveries() {
4639                self.set_plain_failure(error.to_string());
4640                cx.notify();
4641            }
4642            return;
4643        }
4644        if self.state.get() == ScriptViewState::Disposed {
4645            return;
4646        }
4647        self.process_window_commands(cx);
4648        self.sync_program();
4649        let generation = self.lifecycle.generation();
4650        let runtime = self.lifecycle.runtime();
4651        let root = self.lifecycle.root_path().clone();
4652        let (deliveries, dirty, pending_dispatch, virtual_requests, repaint) = {
4653            let mut runtime = runtime.borrow_mut();
4654            runtime.flush_geometry_dependencies();
4655            let _ = runtime.assets.retain_decode_generation(generation);
4656            let now = runtime.clock.now();
4657            let mut deliveries = runtime.tasks.drain(generation);
4658            deliveries.extend(runtime.subscriptions.drain(generation));
4659            deliveries.extend(runtime.timers.drain(now, generation));
4660            runtime.trace_subscription_closures();
4661            if let Ok(asset_deliveries) = runtime.assets.drain_image_decodes(generation) {
4662                deliveries.extend(asset_deliveries);
4663            }
4664            runtime.queue_async(deliveries);
4665            let deliveries = runtime.take_window_async(&self.window_id, &root);
4666            (
4667                deliveries,
4668                runtime.has_window_dirty(&root),
4669                runtime.has_pending_dispatch(),
4670                runtime.has_virtual_requests(),
4671                runtime.take_window_repaint(&self.window_id),
4672            )
4673        };
4674        let has_script_work =
4675            !deliveries.is_empty() || dirty || pending_dispatch || virtual_requests;
4676        if !has_script_work && !repaint {
4677            return;
4678        }
4679
4680        let (delivery_changed, delivery_error) = self.deliver_async_batch(deliveries);
4681        let result = if dirty || pending_dispatch || virtual_requests {
4682            self.run_script_transaction(|view| {
4683                let mut changed = view.invoke_pending_effects()?;
4684                let result = view.lifecycle.realize_virtual_requests(&mut view.engine);
4685                changed |= result.map_err(|error| view.lifecycle_failure(&error, None))?;
4686                let result = view.lifecycle.render_dirty(&mut view.engine);
4687                changed |= result.map_err(|error| view.lifecycle_failure(&error, None))?;
4688                Ok(changed || delivery_changed)
4689            })
4690        } else {
4691            Ok(delivery_changed)
4692        };
4693        let result = match (result, delivery_error) {
4694            (Ok(_), Some(error)) => Err(error),
4695            (result, _) => result,
4696        };
4697        let notify = match result {
4698            Ok(changed) => {
4699                if has_script_work {
4700                    self.clear_failure();
4701                }
4702                self.process_window_commands(cx);
4703                changed || repaint
4704            }
4705            Err(error) => {
4706                self.set_failure(error);
4707                true
4708            }
4709        };
4710        self.collect_timings();
4711        if notify {
4712            cx.notify();
4713        }
4714    }
4715
4716    fn deliver_async_batch(
4717        &mut self,
4718        deliveries: Vec<crate::AsyncDelivery>,
4719    ) -> (bool, Option<ScriptFailure>) {
4720        let mut changed = false;
4721        let mut first_error = None;
4722        for delivery in deliveries {
4723            let scope = format!("{:?}", delivery.scope);
4724            let component = delivery
4725                .callback
4726                .component()
4727                .cloned()
4728                .or_else(|| delivery.scope.component().cloned());
4729            let result = self.run_script_transaction(|view| {
4730                let mut delivery_changed = view.invoke_pending_effects()?;
4731                let result = view.lifecycle.invoke_async_delivery(&view.engine, delivery);
4732                let _ =
4733                    result.map_err(|error| view.lifecycle_failure(&error, component.as_ref()))?;
4734                delivery_changed |= view.invoke_pending_effects()?;
4735                let result = view.lifecycle.render_dirty(&mut view.engine);
4736                delivery_changed |= result.map_err(|error| view.lifecycle_failure(&error, None))?;
4737                Ok(delivery_changed)
4738            });
4739            match result {
4740                Ok(delivery_changed) => changed |= delivery_changed,
4741                Err(error) => {
4742                    self.lifecycle.runtime().borrow_mut().traces.push(
4743                        crate::RuntimeTraceKind::Task,
4744                        scope,
4745                        format!("delivery failed: {}", error.message),
4746                        None,
4747                        false,
4748                    );
4749                    first_error.get_or_insert(error);
4750                }
4751            }
4752        }
4753        (changed, first_error)
4754    }
4755
4756    fn sync_program(&mut self) {
4757        let program = self.factory.program();
4758        if program.compiled.generation() == self.lifecycle.generation() {
4759            return;
4760        }
4761        let previous_exports = match self.engine.component_exports() {
4762            Ok(exports) => exports,
4763            Err(error) => {
4764                self.set_plain_failure(error.to_string());
4765                return;
4766            }
4767        };
4768        let previous_renderers = match self.engine.component_renderer_snapshot() {
4769            Ok(renderers) => renderers,
4770            Err(error) => {
4771                self.set_plain_failure(error.to_string());
4772                return;
4773            }
4774        };
4775        if let Err(error) = self
4776            .engine
4777            .restore_component_exports(program.component_exports.clone())
4778        {
4779            self.set_plain_failure(error.to_string());
4780            return;
4781        }
4782        if let Err(error) = self
4783            .engine
4784            .restore_component_renderers(program.component_renderers.clone())
4785        {
4786            self.set_plain_failure(error.to_string());
4787            let _ = self.engine.restore_component_exports(previous_exports);
4788            return;
4789        }
4790        if let Err(error) =
4791            self.lifecycle
4792                .reload(&mut self.engine, program.compiled, &program.state_schema)
4793        {
4794            let mut failure = self.lifecycle_failure(&error, None);
4795            let rollback_exports = self.engine.restore_component_exports(previous_exports);
4796            let rollback_renderers = self.engine.restore_component_renderers(previous_renderers);
4797            if let Some(rollback) = rollback_exports.err().or_else(|| rollback_renderers.err()) {
4798                failure = failure.append_context(format!("rollback failed: {rollback}"));
4799            }
4800            self.set_failure(failure);
4801        }
4802    }
4803
4804    #[cfg(feature = "dev-reload")]
4805    fn poll_reload(&mut self, cx: &mut Context<Self>) {
4806        let Some(watcher) = &self.watcher else {
4807            return;
4808        };
4809        let batch = match watcher.poll() {
4810            Ok(batch) => batch,
4811            Err(error) => {
4812                self.set_plain_failure(error.to_string());
4813                cx.notify();
4814                return;
4815            }
4816        };
4817        if batch.paths.is_empty() {
4818            return;
4819        }
4820        if self.state.get() == ScriptViewState::Suspended {
4821            self.pending_reload_paths.extend(batch.paths);
4822            return;
4823        }
4824        let result = self.reload_changed_paths(&batch.paths);
4825        match result {
4826            Ok(()) => self.clear_failure(),
4827            Err(error) => self.set_failure(error),
4828        }
4829        self.collect_timings();
4830        cx.notify();
4831    }
4832
4833    #[cfg(feature = "dev-reload")]
4834    fn apply_pending_reload_on_resume(&mut self) -> Option<ScriptFailure> {
4835        let paths = std::mem::take(&mut self.pending_reload_paths);
4836        if paths.is_empty() {
4837            return None;
4838        }
4839        // A broken edit does not strand the retained view. Resume keeps using
4840        // the last successfully compiled program and exposes the candidate
4841        // error through the normal development error banner.
4842        self.reload_changed_paths(&paths).err()
4843    }
4844
4845    #[cfg(feature = "dev-reload")]
4846    fn reload_changed_paths(&mut self, paths: &BTreeSet<PathBuf>) -> Result<(), ScriptFailure> {
4847        let theme_path = self.theme_path.canonicalize().ok();
4848        let style_path = self.style_path.canonicalize().ok();
4849        let themes_root = self.ui_root.join("themes").canonicalize().ok();
4850        let theme_changed = theme_path
4851            .as_ref()
4852            .is_some_and(|theme| paths.contains(theme))
4853            || themes_root
4854                .as_ref()
4855                .is_some_and(|themes_root| paths.iter().any(|path| path.starts_with(themes_root)));
4856        let style_changed = style_path
4857            .as_ref()
4858            .is_some_and(|styles| paths.contains(styles))
4859            || paths.iter().any(|path| path.ends_with(&self.style_path));
4860        let locale_root = self.ui_root.join("locales").canonicalize().ok();
4861        let locale_changed = locale_root
4862            .as_ref()
4863            .is_some_and(|locale_root| paths.iter().any(|path| path.starts_with(locale_root)));
4864        let assets_root = self.ui_root.join("assets").canonicalize().ok();
4865        let assets_changed = assets_root
4866            .as_ref()
4867            .is_some_and(|assets_root| paths.iter().any(|path| path.starts_with(assets_root)));
4868        let manifest_path = self.ui_root.join("app.toml").canonicalize().ok();
4869        let manifest_changed = manifest_path
4870            .as_ref()
4871            .is_some_and(|manifest| paths.contains(manifest));
4872        let script_changed = paths.iter().any(|path| {
4873            path.extension().and_then(|extension| extension.to_str()) == Some("rhai")
4874                && Some(path) != theme_path.as_ref()
4875                && Some(path) != style_path.as_ref()
4876                && !path.ends_with(&self.style_path)
4877                && !locale_root
4878                    .as_ref()
4879                    .is_some_and(|locale_root| path.starts_with(locale_root))
4880                && !themes_root
4881                    .as_ref()
4882                    .is_some_and(|themes_root| path.starts_with(themes_root))
4883        });
4884
4885        if script_changed {
4886            self.reload_scripts(paths)
4887        } else {
4888            Ok(())
4889        }
4890        .and_then(|()| {
4891            if theme_changed {
4892                self.reload_theme().map_err(ScriptFailure::from)
4893            } else {
4894                Ok(())
4895            }
4896        })
4897        .and_then(|()| {
4898            if style_changed {
4899                self.reload_component_styles()
4900            } else {
4901                Ok(())
4902            }
4903        })
4904        .and_then(|()| {
4905            if locale_changed {
4906                self.reload_locales().map_err(ScriptFailure::from)
4907            } else {
4908                Ok(())
4909            }
4910        })
4911        .and_then(|()| {
4912            if assets_changed {
4913                self.reload_assets().map_err(ScriptFailure::from)
4914            } else {
4915                Ok(())
4916            }
4917        })
4918        .and_then(|()| {
4919            if manifest_changed {
4920                self.reload_manifest().map_err(ScriptFailure::from)
4921            } else {
4922                Ok(())
4923            }
4924        })
4925    }
4926
4927    #[cfg(feature = "dev-reload")]
4928    fn reload_scripts(&mut self, changed_paths: &BTreeSet<PathBuf>) -> Result<(), ScriptFailure> {
4929        let source = fs::read_to_string(&self.entry)
4930            .map_err(|error| ScriptFailure::plain(error.to_string()))?;
4931        let modules = discover_modules(
4932            &self.ui_root,
4933            &self.entry,
4934            &self.theme_path,
4935            &self.style_path,
4936        )
4937        .map_err(|error| ScriptFailure::plain(error.to_string()))?;
4938        let file_source = FileScriptSource::new(&self.ui_root, modules)
4939            .map_err(|error| ScriptFailure::plain(error.to_string()))?;
4940        let root = self
4941            .ui_root
4942            .canonicalize()
4943            .map_err(|error| ScriptFailure::plain(error.to_string()))?;
4944        let changed_modules = changed_module_ids(&root, changed_paths);
4945        let mut candidate_engine = self.engine.candidate_engine();
4946        for extension in self.factory.extensions.iter() {
4947            extension
4948                .configure_engine(&mut candidate_engine)
4949                .map_err(ScriptFailure::plain)?;
4950        }
4951        let mut candidate_cache = self.module_cache.clone();
4952        let refresh = candidate_cache
4953            .refresh(candidate_engine.engine(), &file_source, changed_modules)
4954            .map_err(|error| ScriptFailure::plain(error.to_string()))?;
4955        let resolver =
4956            RestrictedModuleResolver::from_source_with_cache(&file_source, &candidate_cache)
4957                .map_err(|error| ScriptFailure::plain(error.to_string()))?;
4958        let candidate = candidate_engine.with_program_preparation(|engine| {
4959            engine.set_module_resolver(resolver);
4960            preload_component_modules(engine, file_source.module_ids())
4961                .map_err(|error| ScriptFailure::plain(error.to_string()))?;
4962            let candidate = engine
4963                .compile_self_contained_named(&self.entry.to_string_lossy(), &source)
4964                .map_err(|error| self.runtime_failure_with_engine(engine, &error, None))?;
4965            let state_schema = engine
4966                .root_state_schema(&candidate)
4967                .map_err(|error| self.runtime_failure_with_engine(engine, &error, None))?;
4968            let program_exports = engine
4969                .component_exports()
4970                .map_err(|error| ScriptFailure::plain(error.to_string()))?;
4971            let program_renderers = engine
4972                .component_renderer_snapshot()
4973                .map_err(|error| ScriptFailure::plain(error.to_string()))?;
4974            Ok::<_, ScriptFailure>((candidate, state_schema, program_exports, program_renderers))
4975        });
4976        let result = candidate.and_then(
4977            |(candidate, state_schema, program_exports, program_renderers)| {
4978                let program_compiled = candidate.clone();
4979                let program_schema = state_schema.clone();
4980                load_file_component_styles(
4981                    candidate_engine.engine(),
4982                    &self.style_path,
4983                    &program_exports,
4984                )
4985                .map_err(|error| ScriptFailure::plain(error.to_string()))?;
4986                if self.state.get() != ScriptViewState::Suspended {
4987                    if let Err(error) =
4988                        self.lifecycle
4989                            .reload(&mut candidate_engine, candidate, &state_schema)
4990                    {
4991                        return Err(self.lifecycle_failure_with_engine(
4992                            &candidate_engine,
4993                            &error,
4994                            None,
4995                        ));
4996                    }
4997                    self.engine = candidate_engine;
4998                }
4999                self.module_cache = candidate_cache;
5000                self.factory.update_program(
5001                    program_compiled,
5002                    program_schema,
5003                    program_exports,
5004                    program_renderers,
5005                );
5006                Ok(())
5007            },
5008        );
5009        if result.is_ok() {
5010            self.trace_script_reload(refresh.affected.len(), refresh.compiled.len());
5011        }
5012        result
5013    }
5014
5015    #[cfg(feature = "dev-reload")]
5016    fn trace_script_reload(&self, affected: usize, compiled: usize) {
5017        self.lifecycle.runtime().borrow_mut().traces.push(
5018            crate::RuntimeTraceKind::Reload,
5019            self.lifecycle.root_path().to_string(),
5020            format!("refreshed {affected} affected module(s), compiled {compiled}"),
5021            None,
5022            false,
5023        );
5024    }
5025
5026    #[cfg(feature = "dev-reload")]
5027    fn reload_theme(&mut self) -> Result<(), String> {
5028        let source = fs::read_to_string(&self.theme_path).map_err(|error| error.to_string())?;
5029        let primary = crate::theme::load_theme_source_with_overrides(
5030            self.engine.engine(),
5031            &self.theme_path.to_string_lossy(),
5032            &source,
5033            &self.factory.theme_token_overrides,
5034        )
5035        .map_err(|error| error.to_string())?;
5036        let runtime = self.lifecycle.runtime();
5037        let previous = runtime
5038            .borrow()
5039            .theme
5040            .as_ref()
5041            .map(|themes| themes.app_preference().clone());
5042        let mut themes = load_theme_directory(
5043            self.engine.engine(),
5044            &self.ui_root.join("themes"),
5045            &primary,
5046            &self.factory.theme_token_overrides,
5047        )
5048        .map_err(|error| error.to_string())?;
5049        if let Some(previous) = previous {
5050            themes
5051                .set_app(previous)
5052                .map_err(|error| error.to_string())?;
5053        }
5054        self.theme = themes
5055            .resolve(
5056                Some(&self.window_id),
5057                Some(&ComponentInstancePath::root("App", &self.window_id)),
5058                SystemAppearance::Dark,
5059            )
5060            .map_err(|error| error.to_string())?
5061            .variant()
5062            .clone();
5063        runtime.borrow_mut().theme = Some(themes);
5064        runtime.borrow_mut().mark_all_windows_dirty();
5065        Ok(())
5066    }
5067
5068    #[cfg(feature = "dev-reload")]
5069    fn reload_component_styles(&mut self) -> Result<(), ScriptFailure> {
5070        let components = self
5071            .engine
5072            .component_exports()
5073            .map_err(|error| ScriptFailure::plain(error.to_string()))?;
5074        let styles = self
5075            .load_current_component_styles(&components)
5076            .map_err(ScriptFailure::from)?;
5077        self.run_script_transaction(|view| {
5078            let changed = view
5079                .lifecycle
5080                .runtime()
5081                .borrow_mut()
5082                .replace_component_styles_from_host(styles);
5083            if changed && view.state.get() == ScriptViewState::Active {
5084                let result = view.lifecycle.render_dirty(&mut view.engine);
5085                result.map_err(|error| view.lifecycle_failure(&error, None))?;
5086            }
5087            Ok(())
5088        })
5089    }
5090
5091    #[cfg(feature = "dev-reload")]
5092    fn load_current_component_styles(
5093        &self,
5094        components: &ComponentRegistry,
5095    ) -> Result<ComponentStyleSheet, String> {
5096        load_file_component_styles(self.engine.engine(), &self.style_path, components)
5097            .map_err(|error| error.to_string())
5098    }
5099
5100    #[cfg(feature = "dev-reload")]
5101    fn reload_locales(&mut self) -> Result<(), String> {
5102        let runtime = self.lifecycle.runtime();
5103        let previous = runtime
5104            .borrow()
5105            .locale
5106            .as_ref()
5107            .map(|locale| locale.app_locale().to_owned());
5108        let mut locales =
5109            load_locale_directory(self.engine.engine(), &self.ui_root.join("locales"))
5110                .map_err(|error| error.to_string())?;
5111        if let (Some(previous), Some(locales)) = (&previous, &mut locales) {
5112            let _ = locales.set_app(previous);
5113        }
5114        let mut runtime = runtime.borrow_mut();
5115        runtime.locale = locales;
5116        runtime.mark_all_windows_dirty();
5117        Ok(())
5118    }
5119
5120    #[cfg(feature = "dev-reload")]
5121    fn reload_assets(&mut self) -> Result<(), String> {
5122        let runtime = self.lifecycle.runtime();
5123        let mut runtime = runtime.borrow_mut();
5124        runtime
5125            .assets
5126            .refresh_namespace("app")
5127            .map_err(|error| error.to_string())?;
5128        runtime.mark_all_windows_dirty();
5129        Ok(())
5130    }
5131
5132    #[cfg(feature = "dev-reload")]
5133    fn reload_manifest(&mut self) -> Result<(), String> {
5134        let manifest =
5135            load_file_manifest(&self.ui_root, &self.entry).map_err(|error| error.to_string())?;
5136        let runtime = self.lifecycle.runtime();
5137        let mut runtime = runtime.borrow_mut();
5138        manifest
5139            .activate(&mut runtime.capabilities)
5140            .map_err(|error| error.to_string())?;
5141        runtime.mark_all_windows_dirty();
5142        Ok(())
5143    }
5144}
5145
5146fn sample_motion_domain(
5147    runtime: &mut UiRuntimeState,
5148    domain: &str,
5149    now: Instant,
5150) -> (crate::MotionFrame, Vec<crate::MotionTimelineEvent>) {
5151    let frame = runtime.motions.tick_scope(now, domain);
5152    runtime.motion_values = runtime.motions.snapshot(now);
5153    let ghosts = std::mem::take(&mut runtime.motion_ghosts);
5154    let mut retained = Vec::with_capacity(ghosts.len());
5155    for ghost in ghosts {
5156        if ghost.domain != domain || runtime.motions.is_node_scope_active(&ghost.path) {
5157            retained.push(ghost);
5158        } else {
5159            runtime.motions.cancel_node_scope(&ghost.path);
5160        }
5161    }
5162    runtime.motion_ghosts = retained;
5163    let events = runtime.motions.drain_timeline_events_for_domain(domain);
5164    (frame, events)
5165}
5166
5167#[cfg(feature = "dev-reload")]
5168fn changed_module_ids(root: &Path, changed_paths: &BTreeSet<PathBuf>) -> Vec<ModuleId> {
5169    changed_paths
5170        .iter()
5171        .filter_map(|path| {
5172            let relative = path.strip_prefix(root).ok()?;
5173            (relative.extension().and_then(|value| value.to_str()) == Some("rhai"))
5174                .then(|| {
5175                    ModuleId::parse(
5176                        relative
5177                            .with_extension("")
5178                            .components()
5179                            .map(|component| component.as_os_str().to_string_lossy())
5180                            .collect::<Vec<_>>()
5181                            .join("/"),
5182                    )
5183                    .ok()
5184                })
5185                .flatten()
5186        })
5187        .collect()
5188}
5189
5190impl Drop for ScriptHostView {
5191    fn drop(&mut self) {
5192        self.release_view();
5193    }
5194}
5195
5196#[derive(Debug, Error)]
5197pub enum ScriptViewError {
5198    #[error("failed to read script entry `{path}`: {source}")]
5199    Io {
5200        path: PathBuf,
5201        #[source]
5202        source: std::io::Error,
5203    },
5204    #[error(transparent)]
5205    Runtime(#[from] RuntimeError),
5206    #[error(transparent)]
5207    Lifecycle(#[from] crate::LifecycleError),
5208    #[error("{message}")]
5209    ScriptDiagnostic {
5210        message: String,
5211        diagnostic: Box<Diagnostic>,
5212    },
5213    #[error("failed to open GPUI window: {0}")]
5214    Window(String),
5215    #[error("script view ID `{0}` must be 1-64 ASCII alphanumeric, `_`, or `-` characters")]
5216    InvalidId(String),
5217    #[error("script view `{0}` is already mounted in this host")]
5218    DuplicateView(String),
5219    #[error("script view `{0}` has been disposed")]
5220    DisposedView(String),
5221    #[error("script view `{0}` is suspended; resume it before rendering or interaction")]
5222    SuspendedView(String),
5223    #[error("script view suspend failed: {0}")]
5224    Suspend(String),
5225    #[error("script view resume failed: {0}")]
5226    Resume(String),
5227    #[error(
5228        "key binding `{keystrokes}` ({context:?}) conflicts: `{existing:?}` is already bound, requested `{requested:?}`"
5229    )]
5230    KeyBindingConflict {
5231        keystrokes: String,
5232        context: Option<String>,
5233        existing: ActionId,
5234        requested: ActionId,
5235    },
5236    #[error("failed to load UI theme: {0}")]
5237    Theme(String),
5238    #[error(transparent)]
5239    ComponentStyle(#[from] ComponentStyleError),
5240    #[error("failed to load UI locale: {0}")]
5241    Locale(String),
5242    #[error("runtime extension failed: {0}")]
5243    Extension(String),
5244    #[error("failed to decode application manifest: {0}")]
5245    Manifest(String),
5246    #[error("application manifest entry `{manifest}` does not match host entry `{host}`")]
5247    ManifestEntry { manifest: ModuleId, host: ModuleId },
5248    #[error(transparent)]
5249    Capability(#[from] CapabilityError),
5250    #[error(transparent)]
5251    Async(#[from] crate::AsyncRuntimeError),
5252    #[error(transparent)]
5253    Responsive(#[from] ResponsiveError),
5254    #[error(transparent)]
5255    Dependency(#[from] DependencyError),
5256    #[error(transparent)]
5257    ComponentExport(#[from] ComponentExportError),
5258    #[error(transparent)]
5259    Action(#[from] ActionError),
5260    #[error(transparent)]
5261    Signal(#[from] crate::SignalError),
5262    #[error(transparent)]
5263    NativeCollection(#[from] crate::NativeCollectionError),
5264    #[error(transparent)]
5265    Document(#[from] crate::DocumentError),
5266    #[error(transparent)]
5267    ElementRef(#[from] crate::ElementRefError),
5268    #[error(transparent)]
5269    Overlay(#[from] crate::OverlayError),
5270    #[error("Rhai module path `{0}` is outside the UI root")]
5271    ModulePath(PathBuf),
5272    #[error(transparent)]
5273    ModuleId(#[from] crate::ModuleIdError),
5274    #[error(transparent)]
5275    ScriptSource(#[from] crate::ScriptSourceError),
5276    #[error(transparent)]
5277    Asset(#[from] crate::AssetError),
5278    #[error(transparent)]
5279    Font(#[from] crate::FontError),
5280    #[error(transparent)]
5281    Accessibility(#[from] crate::AccessibilityError),
5282    #[error(transparent)]
5283    Automation(#[from] crate::AutomationError),
5284    #[cfg(feature = "dev-reload")]
5285    #[error(transparent)]
5286    Watcher(#[from] crate::WatcherError),
5287}
5288
5289impl ScriptViewError {
5290    #[must_use]
5291    pub fn diagnostic(&self) -> Option<&Diagnostic> {
5292        match self {
5293            Self::ScriptDiagnostic { diagnostic, .. } => Some(diagnostic),
5294            _ => None,
5295        }
5296    }
5297}
5298
5299impl From<ScriptViewError> for ScriptFailure {
5300    fn from(error: ScriptViewError) -> Self {
5301        match error {
5302            ScriptViewError::ScriptDiagnostic {
5303                message,
5304                diagnostic,
5305            } => Self::with_diagnostic(message, *diagnostic),
5306            error => Self::plain(error.to_string()),
5307        }
5308    }
5309}
5310
5311#[cfg(test)]
5312mod tests {
5313    use super::*;
5314    use crate::WindowCommand;
5315
5316    #[test]
5317    fn motion_frame_freezes_only_events_present_at_its_sample_boundary() {
5318        let now = Instant::now();
5319        let domain = "window:w/view:v/root";
5320        let source = |name: &str| {
5321            crate::MotionTimeline::new(
5322                name,
5323                crate::MotionTimelineStep::Track(crate::MotionTrack {
5324                    target: ".".to_owned(),
5325                    source: crate::MotionSource::Transition(crate::MotionTransition::new(
5326                        crate::MotionProperty::Opacity,
5327                        0.0,
5328                        1.0,
5329                        1,
5330                    )),
5331                }),
5332            )
5333        };
5334        let mut runtime = UiRuntimeState::new();
5335        runtime
5336            .motions
5337            .start_timeline(
5338                ComponentInstancePath::root("UiNode", format!("{domain}/first")),
5339                source("first"),
5340                now,
5341            )
5342            .unwrap();
5343        let (_, committed) = sample_motion_domain(
5344            &mut runtime,
5345            domain,
5346            now + std::time::Duration::from_millis(1),
5347        );
5348        assert_eq!(committed.len(), 1);
5349
5350        let later = runtime
5351            .motions
5352            .start_timeline(
5353                ComponentInstancePath::root("UiNode", format!("{domain}/later")),
5354                source("later"),
5355                now,
5356            )
5357            .unwrap();
5358        runtime.motions.cancel_timeline(&later).unwrap();
5359        assert_eq!(committed.len(), 1, "the committed batch is immutable");
5360        assert_eq!(
5361            runtime
5362                .motions
5363                .drain_timeline_events_for_domain(domain)
5364                .len(),
5365            1,
5366            "events created after sampling remain for the next rendered frame"
5367        );
5368    }
5369
5370    fn write_manifest(directory: &Path) {
5371        fs::write(
5372            directory.join("app.toml"),
5373            "entry = \"main\"\nruntime_api = 2\n",
5374        )
5375        .unwrap();
5376    }
5377
5378    fn start_prepared(
5379        mut prepared: PreparedScriptView,
5380        view_id: &str,
5381        window_id: &str,
5382    ) -> (RuntimeEngine, ScriptLifecycle) {
5383        let program = prepared.factory.program();
5384        let lifecycle = prepared
5385            .factory
5386            .mount_lifecycle(
5387                &mut prepared.engine,
5388                program,
5389                view_id,
5390                window_id,
5391                WindowCommandPolicy::Disabled,
5392                true,
5393            )
5394            .unwrap();
5395        (prepared.engine, lifecycle)
5396    }
5397
5398    #[test]
5399    fn error_banner_policy_defaults_on_and_can_be_disabled() {
5400        assert!(ScriptViewConfig::new("default").show_error_banner);
5401        assert!(
5402            !ScriptViewConfig::new("custom")
5403                .show_error_banner(false)
5404                .show_error_banner
5405        );
5406    }
5407
5408    #[test]
5409    fn structured_script_error_keeps_human_display_separate_from_diagnostic() {
5410        let mut engine = RuntimeEngine::new();
5411        let compiled = engine
5412            .compile_named("ui/failure.rhai", "fn view() { throw \"boom\"; }")
5413            .unwrap();
5414        let runtime_error = engine.render(&compiled).unwrap_err();
5415        let diagnostic = Diagnostic::from_runtime(
5416            &runtime_error,
5417            &crate::DiagnosticContext {
5418                source: Some("ui/failure.rhai".to_owned()),
5419                component: Some(ComponentInstancePath::root("View", "failure")),
5420                execution_timing: engine.last_failed_timing(),
5421                ..crate::DiagnosticContext::default()
5422            },
5423        );
5424        let error = ScriptViewError::ScriptDiagnostic {
5425            message: "human message".to_owned(),
5426            diagnostic: Box::new(diagnostic.clone()),
5427        };
5428
5429        assert_eq!(error.to_string(), "human message");
5430        assert_eq!(error.diagnostic(), Some(&diagnostic));
5431        let failure = ScriptFailure::from(error);
5432        assert_eq!(failure.message, "human message");
5433        assert_eq!(failure.diagnostic, Some(Box::new(diagnostic)));
5434    }
5435
5436    #[test]
5437    fn initial_mount_failure_returns_a_structured_root_diagnostic() {
5438        let entry = ModuleId::parse("main").unwrap();
5439        let mut prepared = EmbeddedScriptView::new(
5440            entry.clone(),
5441            EmbeddedScriptSource::new(BTreeMap::from([(
5442                entry,
5443                "fn view(ctx) { throw \"mount failure\"; }".to_owned(),
5444            )])),
5445            include_str!("../../../registry/themes/default_dark.rhai"),
5446        )
5447        .prepare()
5448        .unwrap();
5449        let program = prepared.factory.program();
5450        let Err(error) = prepared.factory.mount_lifecycle(
5451            &mut prepared.engine,
5452            program,
5453            "broken-view",
5454            "main",
5455            WindowCommandPolicy::Disabled,
5456            true,
5457        ) else {
5458            panic!("mount unexpectedly succeeded");
5459        };
5460
5461        assert!(error.to_string().contains("mount failure"));
5462        assert!(!error.to_string().contains("component_state"));
5463        let diagnostic = error.diagnostic().expect("script diagnostic");
5464        assert_eq!(diagnostic.component.as_deref(), Some("/View[broken-view]"));
5465        assert_eq!(diagnostic.key.as_deref(), Some("broken-view"));
5466        assert_eq!(
5467            diagnostic
5468                .execution
5469                .as_ref()
5470                .map(|execution| &execution.operation),
5471            Some(&crate::ExecutionOperation::Render)
5472        );
5473    }
5474
5475    #[test]
5476    fn theme_handle_state_only_advances_for_an_effective_theme_change() {
5477        let engine = RuntimeEngine::new();
5478        let dark = crate::load_theme_source(
5479            engine.engine(),
5480            "default_dark.rhai",
5481            include_str!("../../../registry/themes/default_dark.rhai"),
5482        )
5483        .unwrap();
5484        let mut light = dark.clone();
5485        light.name = "Light".to_owned();
5486        light.mode = crate::ThemeMode::Light;
5487        let mut state = ThemeHandleState {
5488            snapshot: ThemeSnapshot::new(7, dark.clone()),
5489        };
5490
5491        assert!(!state.replace(dark));
5492        assert_eq!(state.snapshot.revision, 7);
5493        assert!(state.replace(light.clone()));
5494        assert_eq!(state.snapshot.revision, 8);
5495        assert_eq!(state.snapshot.variant, light);
5496        assert!(state.snapshot.variant.tokens.radii.contains_key("md"));
5497        assert!(
5498            state
5499                .snapshot
5500                .variant
5501                .tokens
5502                .typography
5503                .roles
5504                .contains_key("body")
5505        );
5506    }
5507
5508    #[test]
5509    fn reduced_motion_environment_values_are_normalized() {
5510        assert_eq!(parse_motion_preference("1"), MotionPreference::Reduced);
5511        assert_eq!(parse_motion_preference("TRUE"), MotionPreference::Reduced);
5512        assert_eq!(
5513            parse_motion_preference("reduced"),
5514            MotionPreference::Reduced
5515        );
5516        assert_eq!(parse_motion_preference("0"), MotionPreference::Normal);
5517    }
5518
5519    #[test]
5520    fn retained_descendant_resolves_nearest_scroll_ancestor() {
5521        let root = crate::UiNode::box_node(vec![
5522            crate::UiNode::box_node(vec![crate::UiNode::text("target").with_key("target")])
5523                .with_key("middle"),
5524        ])
5525        .with_key("scroll")
5526        .with_style(&crate::Style::new().overflow_y_scroll());
5527        let mut tree = crate::RetainedUiTree::new();
5528        tree.reconcile(root).unwrap();
5529        let target = tree
5530            .nodes()
5531            .find(|node| node.key() == Some("target"))
5532            .unwrap()
5533            .id();
5534        assert_eq!(nearest_scroll_ancestor(&tree, target), tree.root_id());
5535    }
5536
5537    #[test]
5538    fn pointer_callback_return_controls_gpui_propagation() {
5539        assert_eq!(
5540            event_response_from_dynamic(&rhai::Dynamic::from("propagate")).propagation(),
5541            crate::PropagationControl::Continue
5542        );
5543        assert_eq!(
5544            event_response_from_dynamic(&rhai::Dynamic::UNIT).propagation(),
5545            crate::PropagationControl::Stop
5546        );
5547        let response = crate::EventResponse::new()
5548            .prevent_default()
5549            .capture_pointer();
5550        assert_eq!(
5551            event_response_from_dynamic(&rhai::Dynamic::from(response)),
5552            response
5553        );
5554    }
5555
5556    #[test]
5557    fn prepared_view_starts_engine_and_lifecycle_on_mount() {
5558        let directory = tempfile::tempdir().unwrap();
5559        let entry = directory.path().join("main.rhai");
5560        fs::write(&entry, "fn view(ctx) { text(\"prepared\") }").unwrap();
5561        fs::write(
5562            directory.path().join("theme.rhai"),
5563            include_str!("../../../registry/themes/default_dark.rhai"),
5564        )
5565        .unwrap();
5566        write_manifest(directory.path());
5567        let prepared = FileScriptView::new(&entry).prepare().unwrap();
5568        let (engine, lifecycle) = start_prepared(prepared, "widget", "main");
5569        assert!(engine.is_current(lifecycle.generation()));
5570        assert!(lifecycle.root().is_some());
5571    }
5572
5573    #[test]
5574    fn embedded_host_theme_overrides_cover_primary_and_additional_variants() {
5575        let entry = ModuleId::parse("main").unwrap();
5576        let scripts = EmbeddedScriptSource::new(BTreeMap::from([(
5577            entry.clone(),
5578            "fn view(ctx) { text(\"theme override\") }".to_owned(),
5579        )]));
5580        let overrides = ThemeTokenOverrides {
5581            colors: BTreeMap::from([(
5582                "accent".to_owned(),
5583                crate::Rgba8::from_rgb_hex(0x00aa_55cc),
5584            )]),
5585            radii: BTreeMap::from([
5586                ("sm".to_owned(), crate::Length::Pixels(4.0)),
5587                ("md".to_owned(), crate::Length::Pixels(7.0)),
5588                ("lg".to_owned(), crate::Length::Pixels(10.0)),
5589            ]),
5590            ..ThemeTokenOverrides::default()
5591        };
5592        let prepared = EmbeddedScriptView::new(
5593            entry,
5594            scripts,
5595            include_str!("../../../registry/themes/default_dark.rhai"),
5596        )
5597        .theme_sources([(
5598            "nord.rhai".to_owned(),
5599            include_str!("../../../registry/themes/nord.rhai").to_owned(),
5600        )])
5601        .theme_token_overrides(overrides)
5602        .prepare()
5603        .unwrap();
5604        assert_eq!(
5605            prepared.theme.tokens.radii["md"],
5606            crate::Length::Pixels(7.0)
5607        );
5608        assert_eq!(
5609            prepared.theme.tokens.color("charts.palette_1"),
5610            Some(crate::Rgba8::from_rgb_hex(0x00aa_55cc))
5611        );
5612
5613        let mut runtime = prepared.factory.runtime.borrow_mut();
5614        let themes = runtime.theme.as_mut().unwrap();
5615        themes
5616            .set_app(crate::ThemePreference::Fixed {
5617                selection: ThemeSelection::new("Nord", "Dark"),
5618            })
5619            .unwrap();
5620        assert_eq!(
5621            themes
5622                .resolve(None, None, SystemAppearance::Dark)
5623                .unwrap()
5624                .variant()
5625                .tokens
5626                .radii["md"],
5627            crate::Length::Pixels(7.0)
5628        );
5629    }
5630
5631    #[test]
5632    fn prepare_resolves_copied_component_modules() {
5633        let directory = tempfile::tempdir().unwrap();
5634        let components = directory.path().join("components");
5635        fs::create_dir_all(&components).unwrap();
5636        fs::write(
5637            components.join("greeting.rhai"),
5638            "fn Greeting() { text(\"hello\") }",
5639        )
5640        .unwrap();
5641        fs::write(
5642            directory.path().join("main.rhai"),
5643            "import \"components/greeting\" as greeting; fn view(ctx) { greeting::Greeting() }",
5644        )
5645        .unwrap();
5646        fs::write(
5647            directory.path().join("theme.rhai"),
5648            include_str!("../../../registry/themes/default_dark.rhai"),
5649        )
5650        .unwrap();
5651        write_manifest(directory.path());
5652
5653        let prepared = FileScriptView::new(directory.path().join("main.rhai"))
5654            .prepare()
5655            .unwrap();
5656        let (_, lifecycle) = start_prepared(prepared, "widget", "main");
5657        let root = lifecycle.root().unwrap();
5658        assert!(matches!(
5659            root.kind(),
5660            crate::UiNodeKind::Text { text } if text == "hello"
5661        ));
5662        assert_eq!(
5663            root.source().map(|source| source.module.as_str()),
5664            Some("components/greeting")
5665        );
5666    }
5667
5668    #[test]
5669    fn file_view_applies_the_validated_component_stylesheet() {
5670        let directory = tempfile::tempdir().unwrap();
5671        let components = directory.path().join("components");
5672        fs::create_dir_all(&components).unwrap();
5673        fs::write(
5674            components.join("button.rhai"),
5675            include_str!("../../../registry/components/button.rhai"),
5676        )
5677        .unwrap();
5678        fs::write(
5679            directory.path().join("main.rhai"),
5680            r#"import "components/button" as button;
5681            fn view(ctx) { button::Button(#{ text: "Styled" }) }
5682            "#,
5683        )
5684        .unwrap();
5685        fs::write(
5686            directory.path().join("styles.rhai"),
5687            r#"fn component_styles() {
5688                #{ "components/button": #{ root: style().height(px(41)) } }
5689            }
5690            "#,
5691        )
5692        .unwrap();
5693        fs::write(
5694            directory.path().join("theme.rhai"),
5695            include_str!("../../../registry/themes/default_dark.rhai"),
5696        )
5697        .unwrap();
5698        write_manifest(directory.path());
5699
5700        let prepared = FileScriptView::new(directory.path().join("main.rhai"))
5701            .prepare()
5702            .unwrap();
5703        let (_, lifecycle) = start_prepared(prepared, "widget", "main");
5704        assert_eq!(
5705            lifecycle.root().unwrap().style().base.height,
5706            Some(crate::LayoutLength::Definite(crate::Length::Pixels(41.0)))
5707        );
5708    }
5709
5710    #[test]
5711    fn secondary_window_engine_restores_compiled_component_exports() {
5712        let entry = ModuleId::parse("main").unwrap();
5713        let button = ModuleId::parse("components/button").unwrap();
5714        let scripts = EmbeddedScriptSource::new(BTreeMap::from([
5715            (
5716                entry.clone(),
5717                r#"
5718                    import "components/button" as button;
5719                    fn view(ctx) {
5720                        button::Button(#{ text: `Window ${ctx.window_id()}` })
5721                    }
5722                "#
5723                .to_owned(),
5724            ),
5725            (
5726                button,
5727                include_str!("../../../registry/components/button.rhai").to_owned(),
5728            ),
5729        ]));
5730        let prepared = EmbeddedScriptView::new(
5731            entry,
5732            scripts,
5733            include_str!("../../../registry/themes/default_dark.rhai"),
5734        )
5735        .prepare()
5736        .unwrap();
5737
5738        prepared
5739            .factory
5740            .runtime
5741            .borrow_mut()
5742            .windows
5743            .request_open(ScriptWindowSpec {
5744                id: "settings".to_owned(),
5745                title: "Settings".to_owned(),
5746                width: 400.0,
5747                height: 300.0,
5748                focus: true,
5749            })
5750            .unwrap();
5751        let (_, lifecycle, _) = prepared
5752            .factory
5753            .instantiate("settings-view", "settings")
5754            .unwrap();
5755        assert!(lifecycle.root().is_some());
5756    }
5757
5758    #[test]
5759    fn startup_rejects_manifest_capabilities_missing_from_host() {
5760        let directory = tempfile::tempdir().unwrap();
5761        fs::write(
5762            directory.path().join("main.rhai"),
5763            "fn view(ctx) { text(\"never starts\") }",
5764        )
5765        .unwrap();
5766        fs::write(
5767            directory.path().join("theme.rhai"),
5768            include_str!("../../../registry/themes/default_dark.rhai"),
5769        )
5770        .unwrap();
5771        fs::write(
5772            directory.path().join("app.toml"),
5773            "entry = \"main\"\nruntime_api = 2\n[capabilities]\n\"app.missing\" = \"*\"\n",
5774        )
5775        .unwrap();
5776
5777        assert!(matches!(
5778            FileScriptView::new(directory.path().join("main.rhai")).prepare(),
5779            Err(ScriptViewError::Capability(CapabilityError::Missing(_)))
5780        ));
5781    }
5782
5783    #[test]
5784    fn embedded_asset_sources_match_file_provider_namespace() {
5785        let entry = ModuleId::parse("main").unwrap();
5786        let scripts = EmbeddedScriptSource::new(BTreeMap::from([(
5787            entry.clone(),
5788            r#"
5789                fn state_schema() {
5790                    #{ fields: #{ image: #{ schema: #{ type: "handle", kind: "image" },
5791                        "default": #{ type: "handle", value: #{ kind: "image", id: 0 } } } } }
5792                }
5793                fn init(ctx) { ctx.set_state("image", ctx.load_image(asset("app/check"))); }
5794                fn view(ctx) { image(ctx.get_state("image")) }
5795            "#
5796            .to_owned(),
5797        )]));
5798        let prepared = EmbeddedScriptView::new(
5799            entry,
5800            scripts,
5801            include_str!("../../../registry/themes/default_dark.rhai"),
5802        )
5803        .asset_sources([(
5804            "check".to_owned(),
5805            AssetData {
5806                mime_type: "image/svg+xml".to_owned(),
5807                bytes: include_bytes!("../../../registry/assets/icons/check.svg").to_vec(),
5808            },
5809        )])
5810        .prepare()
5811        .unwrap();
5812        let (_, lifecycle) = start_prepared(prepared, "asset-view", "main");
5813        assert!(matches!(
5814            lifecycle.root().unwrap().kind(),
5815            crate::UiNodeKind::Image {
5816                source: crate::ImageSourceSpec::Handle(handle),
5817            } if handle.id() != 0
5818        ));
5819    }
5820
5821    #[test]
5822    fn embedded_font_sources_are_validated_and_retained_for_mount() {
5823        let entry = ModuleId::parse("main").unwrap();
5824        let scripts = EmbeddedScriptSource::new(BTreeMap::from([(
5825            entry.clone(),
5826            "fn view(ctx) { text(\"font probe\") }".to_owned(),
5827        )]));
5828        let font =
5829            crate::FontSource::new("Art Display", [b"OTTO".as_slice(), &[0, 1, 2, 3]].concat())
5830                .unwrap();
5831        let prepared = EmbeddedScriptView::new(
5832            entry,
5833            scripts,
5834            include_str!("../../../registry/themes/default_dark.rhai"),
5835        )
5836        .font_source(font)
5837        .prepare()
5838        .unwrap();
5839        assert_eq!(prepared.fonts.len(), 1);
5840        assert_eq!(prepared.fonts[0].label(), "Art Display");
5841    }
5842
5843    #[test]
5844    fn embedded_runtime_clock_is_installed_before_initial_render() {
5845        let entry = ModuleId::parse("main").unwrap();
5846        let scripts = EmbeddedScriptSource::new(BTreeMap::from([(
5847            entry.clone(),
5848            "fn view(ctx) { text(\"clock probe\") }".to_owned(),
5849        )]));
5850        let start = std::time::Instant::now();
5851        let manual = crate::ManualRuntimeClock::new(start);
5852        let prepared = EmbeddedScriptView::new(
5853            entry,
5854            scripts,
5855            include_str!("../../../registry/themes/default_dark.rhai"),
5856        )
5857        .runtime_clock(manual.clock())
5858        .prepare()
5859        .unwrap();
5860
5861        manual.advance(std::time::Duration::from_millis(48));
5862        assert_eq!(
5863            prepared.factory.runtime.borrow().clock.now(),
5864            start + std::time::Duration::from_millis(48)
5865        );
5866    }
5867
5868    #[test]
5869    fn declared_component_assets_preload_and_render_without_lifecycle_io() {
5870        let entry = ModuleId::parse("main").unwrap();
5871        let component = ModuleId::parse("components/declarative_icon").unwrap();
5872        let scripts = EmbeddedScriptSource::new(BTreeMap::from([
5873            (
5874                entry.clone(),
5875                r#"
5876                    import "components/declarative_icon" as declarative_icon;
5877                    fn view(ctx) { declarative_icon::DeclarativeIcon(#{} ) }
5878                "#
5879                .to_owned(),
5880            ),
5881            (
5882                component,
5883                r#"/* gpui-rhai
5884{
5885  "id": "components/declarative_icon",
5886  "export": "DeclarativeIcon",
5887  "version": "0.1.0",
5888  "runtime_api": { "min_inclusive": 2, "max_exclusive": 3 },
5889  "dependencies": [],
5890  "capabilities": {},
5891  "assets": ["icons/check.svg"]
5892}
5893*/
5894define_component(#{
5895    metadata: #{ id: "components/declarative_icon", "export": "DeclarativeIcon",
5896        version: "0.1.0", runtime_api: #{ min_inclusive: 2, max_exclusive: 3 },
5897        dependencies: [], capabilities: #{}, assets: ["icons/check.svg"] },
5898    schema: #{ props: #{}, state: #{ fields: #{} }, events: #{}, slots: #{}, parts: ["root"] },
5899    render: Fn("render_DeclarativeIcon")
5900});
5901fn DeclarativeIcon(props) { render_component("components/declarative_icon", props) }
5902fn render_DeclarativeIcon(ctx, props) { image(asset("app/icons/check")) }
5903"#
5904                .to_owned(),
5905            ),
5906        ]));
5907        let prepared = EmbeddedScriptView::new(
5908            entry,
5909            scripts,
5910            include_str!("../../../registry/themes/default_dark.rhai"),
5911        )
5912        .asset_sources([(
5913            "icons/check".to_owned(),
5914            AssetData {
5915                mime_type: "image/svg+xml".to_owned(),
5916                bytes: include_bytes!("../../../registry/assets/icons/check.svg").to_vec(),
5917            },
5918        )])
5919        .prepare()
5920        .unwrap();
5921        assert!(
5922            prepared
5923                .factory
5924                .runtime
5925                .borrow()
5926                .assets
5927                .cached_image(&AssetId::parse("app/icons/check").unwrap())
5928                .is_ok()
5929        );
5930        let (_, lifecycle) = start_prepared(prepared, "asset-view", "main");
5931        assert!(matches!(
5932            lifecycle.root().unwrap().kind(),
5933            crate::UiNodeKind::Image {
5934                source: crate::ImageSourceSpec::Asset(asset),
5935            } if asset.as_str() == "app/icons/check"
5936        ));
5937    }
5938
5939    #[test]
5940    fn missing_declared_component_asset_fails_preparation() {
5941        let entry = ModuleId::parse("main").unwrap();
5942        let component = ModuleId::parse("components/missing_asset").unwrap();
5943        let scripts = EmbeddedScriptSource::new(BTreeMap::from([
5944            (
5945                entry.clone(),
5946                "import \"components/missing_asset\" as missing; fn view(ctx) { missing::MissingAsset(#{} ) }"
5947                    .to_owned(),
5948            ),
5949            (
5950                component,
5951                r#"/* gpui-rhai
5952{
5953  "id": "components/missing_asset", "export": "MissingAsset", "version": "0.1.0",
5954  "runtime_api": { "min_inclusive": 2, "max_exclusive": 3 },
5955  "dependencies": [], "capabilities": {}, "assets": ["icons/missing.svg"]
5956}
5957*/
5958define_component(#{ metadata: #{ id: "components/missing_asset", "export": "MissingAsset",
5959    version: "0.1.0", runtime_api: #{ min_inclusive: 2, max_exclusive: 3 },
5960    dependencies: [], capabilities: #{}, assets: ["icons/missing.svg"] },
5961    schema: #{ props: #{}, state: #{ fields: #{} }, events: #{}, slots: #{}, parts: ["root"] },
5962    render: Fn("render_MissingAsset") });
5963fn MissingAsset(props) { render_component("components/missing_asset", props) }
5964fn render_MissingAsset(ctx, props) { image(asset("app/icons/missing")) }
5965"#
5966                .to_owned(),
5967            ),
5968        ]));
5969        let result = EmbeddedScriptView::new(
5970            entry,
5971            scripts,
5972            include_str!("../../../registry/themes/default_dark.rhai"),
5973        )
5974        .prepare();
5975        assert!(matches!(result, Err(ScriptViewError::Asset(_))));
5976    }
5977
5978    #[test]
5979    fn file_and_embedded_apps_produce_equivalent_locale_asset_tree() {
5980        let script = r#"
5981            fn state_schema() {
5982                #{ fields: #{ image: #{ schema: #{ type: "handle", kind: "image" },
5983                    "default": #{ type: "handle", value: #{ kind: "image", id: 0 } } } } }
5984            }
5985            fn init(ctx) { ctx.set_state("image", ctx.load_image(asset("app/check"))); }
5986            fn view(ctx) { row([text(ctx.t("common.loading")), image(ctx.get_state("image"))]) }
5987        "#;
5988        let locale = include_str!("../../../registry/locales/en.rhai");
5989        let theme = include_str!("../../../registry/themes/default_dark.rhai");
5990        let svg = include_bytes!("../../../registry/assets/icons/check.svg");
5991        let directory = tempfile::tempdir().unwrap();
5992        fs::create_dir_all(directory.path().join("locales")).unwrap();
5993        fs::create_dir_all(directory.path().join("assets")).unwrap();
5994        fs::write(directory.path().join("main.rhai"), script).unwrap();
5995        fs::write(directory.path().join("theme.rhai"), theme).unwrap();
5996        fs::write(
5997            directory.path().join("app.toml"),
5998            "entry = \"main\"\nruntime_api = 2\n",
5999        )
6000        .unwrap();
6001        fs::write(directory.path().join("locales/en.rhai"), locale).unwrap();
6002        fs::write(directory.path().join("assets/check.svg"), svg).unwrap();
6003        let file = FileScriptView::new(directory.path().join("main.rhai"))
6004            .prepare()
6005            .unwrap();
6006
6007        let entry = ModuleId::parse("main").unwrap();
6008        let embedded = EmbeddedScriptView::new(
6009            entry.clone(),
6010            EmbeddedScriptSource::new(BTreeMap::from([(entry, script.to_owned())])),
6011            theme,
6012        )
6013        .locale_sources([("locales/en.rhai".to_owned(), locale.to_owned())])
6014        .asset_sources([(
6015            "check".to_owned(),
6016            AssetData {
6017                mime_type: "image/svg+xml".to_owned(),
6018                bytes: svg.to_vec(),
6019            },
6020        )])
6021        .prepare()
6022        .unwrap();
6023
6024        let (_, file) = start_prepared(file, "file-view", "main");
6025        let (_, embedded) = start_prepared(embedded, "embedded-view", "main");
6026
6027        for root in [file.root().unwrap(), embedded.root().unwrap()] {
6028            let crate::UiNodeKind::Box { children } = root.kind() else {
6029                panic!("equivalence app must render a row");
6030            };
6031            assert!(
6032                matches!(&children[0].kind(), crate::UiNodeKind::Text { text } if text == "Loading")
6033            );
6034            assert!(matches!(
6035                &children[1].kind(),
6036                crate::UiNodeKind::Image {
6037                    source: crate::ImageSourceSpec::Handle(handle),
6038                } if handle.id() == 1
6039            ));
6040        }
6041    }
6042
6043    #[test]
6044    fn missing_entry_has_path_aware_error() {
6045        let path = Path::new("definitely-missing-ui/main.rhai");
6046        let Err(error) = FileScriptView::new(path).prepare() else {
6047            panic!("missing script unexpectedly prepared");
6048        };
6049        assert!(
6050            error
6051                .to_string()
6052                .contains("definitely-missing-ui/main.rhai")
6053        );
6054    }
6055
6056    #[test]
6057    fn script_window_api_queues_open_and_installs_close_confirmation_handler() {
6058        let mut engine = RuntimeEngine::new();
6059        let compiled = engine
6060            .compile(
6061                r#"
6062                    fn state_schema() { #{ fields: #{
6063                        close_pending: #{ schema: #{ type: "bool" },
6064                            "default": #{ type: "bool", value: false } }
6065                    } } }
6066                    fn close_requested(ctx, payload) {
6067                        ctx.set_state("close_pending", true);
6068                    }
6069                    fn init(ctx) {
6070                        ctx.set_close_handler(Fn("close_requested"));
6071                        ctx.open_window("settings", "Settings", 640, 480, true);
6072                    }
6073                    fn view(ctx) { text(ctx.window_id()) }
6074                "#,
6075            )
6076            .unwrap();
6077        let state_schema = engine.root_state_schema(&compiled).unwrap();
6078        let mut runtime = UiRuntimeState::new();
6079        runtime.windows.register_open("main").unwrap();
6080        let runtime = Rc::new(RefCell::new(runtime));
6081        let mut lifecycle = ScriptLifecycle::new(
6082            compiled,
6083            Rc::clone(&runtime),
6084            ComponentInstancePath::root("App", "main"),
6085            Some("main".to_owned()),
6086            BTreeMap::new(),
6087            &state_schema,
6088        )
6089        .unwrap();
6090        lifecycle.start(&mut engine).unwrap();
6091
6092        let close_handler = runtime.borrow().windows.close_handler("main").unwrap();
6093        let _ = lifecycle
6094            .invoke_callback_transactional(&engine, &close_handler, UiValue::Null)
6095            .unwrap();
6096        assert_eq!(
6097            runtime
6098                .borrow()
6099                .component_state
6100                .get(&ComponentInstancePath::root("App", "main"), "close_pending"),
6101            Some(&UiValue::Bool(true))
6102        );
6103
6104        let mut runtime = runtime.borrow_mut();
6105        assert!(runtime.windows.close_handler("main").is_some());
6106        assert!(matches!(
6107            runtime.windows.drain_commands().as_slice(),
6108            [WindowCommand::Open(spec)] if spec.id == "settings" && spec.focus
6109        ));
6110    }
6111
6112    #[test]
6113    fn script_registered_semantic_action_dispatches_generation_bound_callback() {
6114        let mut engine = RuntimeEngine::new();
6115        let compiled = engine
6116            .compile(
6117                r#"
6118                    fn state_schema() {
6119                        #{ fields: #{ count: #{ schema: #{ type: "integer" },
6120                            "default": #{ type: "integer", value: 0 } } } }
6121                    }
6122                    fn increment(ctx, payload) {
6123                        ctx.set_state("count", ctx.get_state("count") + 1);
6124                    }
6125                    fn fire(ctx, payload) { ctx.dispatch_action("counter.increment", ()); }
6126                    fn init(ctx) { ctx.register_action("counter.increment", Fn("increment")); }
6127                    fn view(ctx) { text(`${ctx.get_state("count")}`).on_click(Fn("fire")) }
6128                "#,
6129            )
6130            .unwrap();
6131        let schema = engine.root_state_schema(&compiled).unwrap();
6132        let runtime = Rc::new(RefCell::new(UiRuntimeState::new()));
6133        let root_path = ComponentInstancePath::root("App", "main");
6134        let mut lifecycle = ScriptLifecycle::new(
6135            compiled,
6136            Rc::clone(&runtime),
6137            root_path.clone(),
6138            Some("main".to_owned()),
6139            BTreeMap::new(),
6140            &schema,
6141        )
6142        .unwrap();
6143        lifecycle.start(&mut engine).unwrap();
6144        let fire = lifecycle
6145            .root()
6146            .unwrap()
6147            .handler("click")
6148            .unwrap()
6149            .as_script()
6150            .unwrap()
6151            .clone();
6152        let _ = lifecycle
6153            .invoke_callback(&engine, &fire, UiValue::Null)
6154            .unwrap();
6155        let actions = runtime.borrow_mut().drain_pending_actions();
6156        for action in actions {
6157            let _ = lifecycle
6158                .invoke_callback(&engine, &action.callback, action.payload)
6159                .unwrap();
6160        }
6161        lifecycle.render(&mut engine).unwrap();
6162        assert_eq!(
6163            runtime.borrow().component_state.get(&root_path, "count"),
6164            Some(&UiValue::Integer(1))
6165        );
6166    }
6167}