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