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