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