Skip to main content

pfx_editor_viewport/
watched.rs

1use std::path::{Path, PathBuf};
2use std::sync::{Arc, Mutex, PoisonError};
3
4use pfx_editor_doc::Stamp;
5use pfx_editor_shell::Wake;
6use pfx_load::scene::{Reload, Scene, SceneError, SceneWatch};
7
8pub type Waker = Arc<Mutex<Option<Wake>>>;
9
10pub fn worded(error: &SceneError, scene: &Path) -> String {
11    let folder = scene.parent().unwrap_or(Path::new(""));
12    let file = error.file.strip_prefix(folder).unwrap_or(&error.file);
13    match error.line {
14        Some(line) => format!("{}:{line}: {}", file.display(), error.message),
15        None => format!("{}: {}", file.display(), error.message),
16    }
17}
18
19pub fn is_scene(path: &Path) -> bool {
20    path.file_name()
21        .is_some_and(|name| name.to_string_lossy().ends_with(".scene.toml"))
22        || path.file_name().is_some_and(|name| name == "scene.toml")
23}
24
25#[derive(Debug)]
26pub enum Event {
27    Reloaded(Box<Reload>),
28    Broken(String),
29}
30
31pub struct Watched {
32    path: PathBuf,
33    watch: Option<SceneWatch>,
34    waker: Waker,
35    error: Option<String>,
36    tried: Option<Option<Stamp>>,
37    reloads: u64,
38}
39
40fn ring(waker: &Waker) {
41    let wake = waker.lock().unwrap_or_else(PoisonError::into_inner).clone();
42    if let Some(wake) = wake {
43        wake();
44    }
45}
46
47impl Watched {
48    pub fn open(path: impl Into<PathBuf>) -> Watched {
49        let mut watched = Watched {
50            path: path.into(),
51            watch: None,
52            waker: Arc::new(Mutex::new(None)),
53            error: None,
54            tried: None,
55            reloads: 0,
56        };
57        watched.reopen();
58        watched
59    }
60
61    fn stamp(&self) -> Option<Stamp> {
62        std::fs::read_to_string(&self.path)
63            .ok()
64            .map(|text| Stamp::of(&text))
65    }
66
67    fn reopen(&mut self) -> bool {
68        self.tried = Some(self.stamp());
69        let waker = self.waker.clone();
70        match SceneWatch::open_with(&self.path, move || ring(&waker)) {
71            Ok(watch) => {
72                self.watch = Some(watch);
73                self.error = None;
74                true
75            }
76            Err(error) => {
77                self.error = Some(worded(&error, &self.path));
78                false
79            }
80        }
81    }
82
83    pub fn set_wake(&mut self, wake: Wake) {
84        *self.waker.lock().unwrap_or_else(PoisonError::into_inner) = Some(wake);
85    }
86
87    pub fn path(&self) -> &Path {
88        &self.path
89    }
90
91    pub fn scene(&self) -> Option<&Scene> {
92        self.watch.as_ref().map(SceneWatch::scene)
93    }
94
95    pub fn error(&self) -> Option<&str> {
96        self.error.as_deref()
97    }
98
99    pub fn reloads(&self) -> u64 {
100        self.reloads
101    }
102
103    pub fn files(&self) -> Vec<PathBuf> {
104        match &self.watch {
105            Some(watch) => watch.files().map(Path::to_path_buf).collect(),
106            None => vec![self.path.clone()],
107        }
108    }
109
110    pub fn check(&mut self) -> Option<Event> {
111        let result = match self.watch.as_mut() {
112            Some(watch) => watch.check(),
113            None => return self.opened(),
114        };
115        self.take(result)
116    }
117
118    pub fn poll(&mut self) -> Option<Event> {
119        if self.watch.is_none() && self.tried == Some(self.stamp()) {
120            return None;
121        }
122        let result = match self.watch.as_mut() {
123            Some(watch) => watch.poll(),
124            None => return self.opened(),
125        };
126        self.take(result)
127    }
128
129    fn opened(&mut self) -> Option<Event> {
130        if !self.reopen() {
131            return None;
132        }
133        let scene = self.scene()?.clone();
134        self.reloads += 1;
135        Some(Event::Reloaded(Box::new(Reload {
136            diff: Scene::diff(&scene, &scene),
137            scene,
138            recovered: true,
139        })))
140    }
141
142    fn take(&mut self, result: Option<Result<Reload, SceneError>>) -> Option<Event> {
143        match result? {
144            Ok(reload) => {
145                self.error = None;
146                self.reloads += 1;
147                Some(Event::Reloaded(Box::new(reload)))
148            }
149            Err(error) => {
150                let worded = worded(&error, &self.path);
151                self.error = Some(worded.clone());
152                Some(Event::Broken(worded))
153            }
154        }
155    }
156}