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