use std::path::{Path, PathBuf};
use std::time::{Duration, SystemTime};
use web_time::Instant;
use crate::Texture;
pub(crate) const ENV_HOT_RELOAD: &str = "CRABBY_HOT_RELOAD";
const CHECK_EVERY: Duration = Duration::from_millis(250);
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
struct Stamp {
modified: Option<SystemTime>,
len: u64,
}
impl Stamp {
fn of(path: &Path) -> Option<Self> {
let meta = std::fs::metadata(path).ok()?;
Some(Stamp {
modified: meta.modified().ok(),
len: meta.len(),
})
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub(crate) enum Asset {
Texture(Texture),
Font(u32),
Sound(u32),
Data(usize),
#[cfg(feature = "3d")]
Shader(u32),
#[cfg(feature = "3d")]
Effect(u32),
#[cfg(feature = "3d")]
Compute(u32),
}
struct Watched {
name: String,
path: PathBuf,
stamp: Option<Stamp>,
asset: Asset,
}
pub(crate) struct Watcher {
pub(crate) on: bool,
files: Vec<Watched>,
next_check: Instant,
pub(crate) reloaded: Vec<Texture>,
pub(crate) reloaded_files: Vec<String>,
pub(crate) pending: Vec<(Asset, PathBuf, String)>,
}
impl Default for Watcher {
fn default() -> Self {
Watcher {
on: false,
files: Vec::new(),
next_check: Instant::now(),
reloaded: Vec::new(),
reloaded_files: Vec::new(),
pending: Vec::new(),
}
}
}
pub(crate) fn enabled(on: bool) -> bool {
if cfg!(target_arch = "wasm32") {
return false;
}
match std::env::var(ENV_HOT_RELOAD).as_deref() {
Ok("1") => true,
Ok("0") => false,
_ => on,
}
}
impl Watcher {
pub(crate) fn watch(&mut self, name: &str, path: PathBuf, asset: Asset) {
if !self.on {
return;
}
let stamp = Stamp::of(&path);
self.files.push(Watched {
name: name.to_string(),
path,
stamp,
asset,
});
}
pub(crate) fn changed(&mut self, now: Instant, out: &mut Vec<(Asset, PathBuf, String)>) {
out.clear();
if !self.on || self.files.is_empty() || now < self.next_check {
return;
}
self.next_check = now + CHECK_EVERY;
for f in &mut self.files {
let stamp = Stamp::of(&f.path);
if stamp.is_some() && stamp != f.stamp {
f.stamp = stamp;
out.push((f.asset, f.path.clone(), f.name.clone()));
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn temp_file(name: &str, bytes: &[u8]) -> PathBuf {
let dir = std::env::temp_dir().join(format!("crabby-hot-{}-{name}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join(name);
std::fs::write(&path, bytes).unwrap();
path
}
#[test]
fn changes_are_reported_once_and_not_before_the_next_check() {
let path = temp_file("a.png", b"one");
let mut w = Watcher {
on: true,
..Default::default()
};
let tex = Texture::new(7, 2, 2);
w.watch("a.png", path.clone(), Asset::Texture(tex));
let mut out = Vec::new();
let t0 = Instant::now();
w.changed(t0, &mut out);
assert!(out.is_empty(), "nothing changed yet");
std::fs::write(&path, b"three").unwrap(); w.changed(t0 + Duration::from_millis(10), &mut out);
assert!(out.is_empty(), "too soon after the last check");
w.changed(t0 + CHECK_EVERY * 2, &mut out);
assert_eq!(out.len(), 1);
assert_eq!(out[0].0, Asset::Texture(tex));
assert_eq!(out[0].2, "a.png");
w.changed(t0 + CHECK_EVERY * 4, &mut out);
assert!(out.is_empty(), "reported once");
}
#[test]
fn missing_files_keep_their_texture() {
let path = temp_file("b.png", b"one");
let mut w = Watcher {
on: true,
..Default::default()
};
w.watch("b.png", path.clone(), Asset::Texture(Texture::new(1, 1, 1)));
std::fs::remove_file(&path).unwrap();
let mut out = Vec::new();
w.changed(Instant::now() + CHECK_EVERY, &mut out);
assert!(out.is_empty());
}
#[test]
fn nothing_is_watched_while_off() {
let path = temp_file("c.png", b"one");
let mut w = Watcher::default();
w.watch("c.png", path, Asset::Data(0));
assert!(w.files.is_empty());
}
}