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