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