diff --git a/crates/ui-egui/src/chrome.rs b/crates/ui-egui/src/chrome.rs index c490895e2..f4423f25d 100644 --- a/crates/ui-egui/src/chrome.rs +++ b/crates/ui-egui/src/chrome.rs @@ -739,8 +739,9 @@ fn recent_files(app: &mut DesignApp, ui: &mut egui::Ui) { let mut open = None; ui.horizontal_wrapped(|ui| { for (i, path) in rows { - let modified = std::fs::metadata(&path).ok().filter(|m| m.is_file()).map(|m| m.modified().ok()); - let there = modified.is_some(); + let stat = recent_file_stat(ui.ctx(), &path); + let there = stat.exists; + let modified = there.then_some(stat.modified); let (cr, resp) = ui.allocate_exact_size(vec2(140.0, 170.0), Sense::click()); let resp = resp.on_hover_text(&path); ui.painter().rect_filled(cr, 6.0, if resp.hovered() { t.hover } else { t.panel }); @@ -778,7 +779,38 @@ fn recent_files(app: &mut DesignApp, ui: &mut egui::Ui) { } } -/// The texture of a recent file's saved first-page picture, read again when the file changes. +/// How often a recent file is looked up on disk again, in ms: the start screen and the Open Recent +/// menu draw every frame and must not touch the file system each time. +const RECENT_STAT_MS: f64 = 3000.0; + +/// A recent file as last seen on disk. +#[derive(Clone, Debug, PartialEq)] +pub(crate) struct RecentStat { + pub exists: bool, + pub modified: Option, + checked: f64, +} + +/// Whether recent file `path` is there and when it last changed, looked up again at most every +/// [`RECENT_STAT_MS`]. +pub(crate) fn recent_file_stat(ctx: &egui::Context, path: &str) -> RecentStat { + recent_file_stat_at(ctx, path, crate::now_ms()) +} + +fn recent_file_stat_at(ctx: &egui::Context, path: &str, now: f64) -> RecentStat { + let key = egui::Id::new(("recent_stat", path)); + if let Some(st) = ctx.data(|d| d.get_temp::(key)) + && (0.0..RECENT_STAT_MS).contains(&(now - st.checked)) + { + return st; + } + let meta = std::fs::metadata(path).ok().filter(std::fs::Metadata::is_file); + let st = RecentStat { exists: meta.is_some(), modified: meta.and_then(|m| m.modified().ok()), checked: now }; + ctx.data_mut(|d| d.insert_temp(key, st.clone())); + st +} + +/// The texture of a recent file's saved first-page picture, decoded once per file version. fn recent_preview_texture(ctx: &egui::Context, path: &str, modified: Option) -> Option { type Cached = (Option, Option); let key = egui::Id::new(("recent_preview", path)); @@ -858,6 +890,22 @@ mod tests { const LAST_CONTROL: &str = "Align bottom edges"; const FIRST_CONTROL: &str = "Rotate 90° Counterclockwise"; + /// The start screen and Open Recent menu look a recent file up on disk at most every few + /// seconds, not every frame. + #[test] + fn recent_files_are_looked_up_on_disk_every_few_seconds() { + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join("a.designcraft"); + std::fs::write(&path, b"x").unwrap(); + let path = path.to_string_lossy().to_string(); + let ctx = egui::Context::default(); + let first = super::recent_file_stat_at(&ctx, &path, 10_000.0); + assert!(first.exists && first.modified.is_some()); + std::fs::remove_file(&path).unwrap(); + assert_eq!(super::recent_file_stat_at(&ctx, &path, 10_000.0 + super::RECENT_STAT_MS - 1.0), first, "cached"); + assert!(!super::recent_file_stat_at(&ctx, &path, 10_000.0 + super::RECENT_STAT_MS).exists, "looked up again"); + } + fn app() -> crate::DesignApp { let mut app = crate::DesignApp::new(designcraft_engine::Session::new(), crate::Services::default()); app.ui.control_bar = true; diff --git a/crates/ui-egui/src/menus.rs b/crates/ui-egui/src/menus.rs index 388ff4ad8..8438ab5e9 100644 --- a/crates/ui-egui/src/menus.rs +++ b/crates/ui-egui/src/menus.rs @@ -2615,7 +2615,7 @@ fn recent_menu_items(app: &mut DesignApp, ui: &mut egui::Ui) { Item::Cmd { label, id, params, .. } => { let path = params["index"].as_u64().and_then(|i| app.session.prefs.recent_files.get(i as usize)).cloned().unwrap_or_default(); let mut text = egui::RichText::new(label); - if !std::path::Path::new(&path).is_file() { + if !crate::chrome::recent_file_stat(ui.ctx(), &path).exists { text = text.color(dim); } if ui.button(crate::rtl::widget(ui, text)).on_hover_text(&path).clicked() {