Skip to main content

fret_runtime/
asset_reload.rs

1use crate::ui_host::GlobalsHost;
2
3/// Global epoch observed by asset-consuming code when logical assets should be reloaded.
4///
5/// This is intentionally generic: UI code, diagnostics, and future non-UI integrations should all
6/// observe the same invalidation token instead of inventing asset-class-specific reload globals.
7#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
8pub struct AssetReloadEpoch(pub u64);
9
10impl AssetReloadEpoch {
11    pub fn bump(&mut self) {
12        self.0 = self.0.wrapping_add(1);
13    }
14}
15
16/// Host-level support flags for development asset reload automation.
17///
18/// `file_watch` means the current host/startup stack will automatically detect native file-backed
19/// asset changes and publish new [`AssetReloadEpoch`] values without requiring an app-local manual
20/// bump.
21#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
22pub struct AssetReloadSupport {
23    pub file_watch: bool,
24}
25
26/// Configured or active backend for automatic development asset reload.
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub enum AssetReloadBackendKind {
29    PollMetadata,
30    NativeWatcher,
31}
32
33/// Reason why the active automatic reload backend differs from the configured one.
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35pub enum AssetReloadFallbackReason {
36    WatcherInstallFailed,
37}
38
39/// Best-effort host-published status for the current automatic asset reload lane.
40///
41/// This is intentionally separate from [`AssetReloadSupport`]:
42/// - `AssetReloadSupport` answers whether the host can automatically publish reload invalidations,
43/// - `AssetReloadStatus` answers which backend is currently active and whether a fallback occurred.
44///
45/// Hosts may leave this unset until the effective backend is known.
46#[derive(Debug, Clone, PartialEq, Eq)]
47pub struct AssetReloadStatus {
48    pub configured_backend: AssetReloadBackendKind,
49    pub active_backend: AssetReloadBackendKind,
50    pub fallback_reason: Option<AssetReloadFallbackReason>,
51    pub fallback_message: Option<String>,
52}
53
54pub fn asset_reload_epoch(host: &impl GlobalsHost) -> Option<AssetReloadEpoch> {
55    host.global::<AssetReloadEpoch>().copied()
56}
57
58pub fn bump_asset_reload_epoch(host: &mut impl GlobalsHost) {
59    host.with_global_mut(AssetReloadEpoch::default, |epoch, _host| {
60        epoch.bump();
61    });
62}
63
64pub fn asset_reload_support(host: &impl GlobalsHost) -> Option<AssetReloadSupport> {
65    host.global::<AssetReloadSupport>().copied()
66}
67
68pub fn set_asset_reload_support(host: &mut impl GlobalsHost, support: AssetReloadSupport) {
69    host.set_global(support);
70}
71
72pub fn asset_reload_status(host: &impl GlobalsHost) -> Option<AssetReloadStatus> {
73    host.global::<AssetReloadStatus>().cloned()
74}
75
76pub fn set_asset_reload_status(host: &mut impl GlobalsHost, status: AssetReloadStatus) {
77    host.set_global(status);
78}
79
80#[cfg(test)]
81mod tests {
82    use std::any::{Any, TypeId};
83    use std::collections::HashMap;
84
85    use super::{
86        AssetReloadBackendKind, AssetReloadEpoch, AssetReloadFallbackReason, AssetReloadStatus,
87        AssetReloadSupport, asset_reload_epoch, asset_reload_status, asset_reload_support,
88        bump_asset_reload_epoch, set_asset_reload_status, set_asset_reload_support,
89    };
90    use crate::ui_host::GlobalsHost;
91
92    #[derive(Default)]
93    struct TestHost {
94        globals: HashMap<TypeId, Box<dyn Any>>,
95    }
96
97    impl GlobalsHost for TestHost {
98        fn set_global<T: Any>(&mut self, value: T) {
99            self.globals.insert(TypeId::of::<T>(), Box::new(value));
100        }
101
102        fn global<T: Any>(&self) -> Option<&T> {
103            self.globals.get(&TypeId::of::<T>())?.downcast_ref::<T>()
104        }
105
106        fn with_global_mut<T: Any, R>(
107            &mut self,
108            init: impl FnOnce() -> T,
109            f: impl FnOnce(&mut T, &mut Self) -> R,
110        ) -> R {
111            let type_id = TypeId::of::<T>();
112            let mut value = match self.globals.remove(&type_id) {
113                None => init(),
114                Some(v) => *v.downcast::<T>().expect("global type id must match"),
115            };
116            let out = f(&mut value, self);
117            self.globals.insert(type_id, Box::new(value));
118            out
119        }
120    }
121
122    #[test]
123    fn bump_asset_reload_epoch_initializes_and_increments_global() {
124        let mut host = TestHost::default();
125        assert_eq!(asset_reload_epoch(&host), None);
126
127        bump_asset_reload_epoch(&mut host);
128        assert_eq!(asset_reload_epoch(&host), Some(AssetReloadEpoch(1)));
129
130        bump_asset_reload_epoch(&mut host);
131        assert_eq!(asset_reload_epoch(&host), Some(AssetReloadEpoch(2)));
132    }
133
134    #[test]
135    fn set_asset_reload_support_publishes_host_support_flags() {
136        let mut host = TestHost::default();
137        assert_eq!(asset_reload_support(&host), None);
138
139        set_asset_reload_support(&mut host, AssetReloadSupport { file_watch: true });
140        assert_eq!(
141            asset_reload_support(&host),
142            Some(AssetReloadSupport { file_watch: true })
143        );
144    }
145
146    #[test]
147    fn set_asset_reload_status_publishes_active_backend_and_fallback() {
148        let mut host = TestHost::default();
149        assert_eq!(asset_reload_status(&host), None);
150
151        set_asset_reload_status(
152            &mut host,
153            AssetReloadStatus {
154                configured_backend: AssetReloadBackendKind::NativeWatcher,
155                active_backend: AssetReloadBackendKind::PollMetadata,
156                fallback_reason: Some(AssetReloadFallbackReason::WatcherInstallFailed),
157                fallback_message: Some("watch root missing".to_string()),
158            },
159        );
160
161        assert_eq!(
162            asset_reload_status(&host),
163            Some(AssetReloadStatus {
164                configured_backend: AssetReloadBackendKind::NativeWatcher,
165                active_backend: AssetReloadBackendKind::PollMetadata,
166                fallback_reason: Some(AssetReloadFallbackReason::WatcherInstallFailed),
167                fallback_message: Some("watch root missing".to_string()),
168            })
169        );
170    }
171}