pfx_editor_viewport/
watched.rs1use 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}