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