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