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