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