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