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