Skip to main content

gpui_rhai/
app.rs

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