diff --git a/crates/ui-egui/src/backstage.rs b/crates/ui-egui/src/backstage.rs index 75ff81bc2..ac07f8424 100644 --- a/crates/ui-egui/src/backstage.rs +++ b/crates/ui-egui/src/backstage.rs @@ -358,6 +358,11 @@ fn open_list(app: &mut WordApp, ui: &mut Ui) { let t = Tokens::get(ui.ctx()); ui.label(egui::RichText::new(tl!("Recent")).font(semibold(16.0))); ui.add_space(6.0); + let error = ui.ctx().data(|d| d.get_temp::(open_error_id())).unwrap_or_default(); + if !error.is_empty() { + ui.label(egui::RichText::new(error).color(ui.visuals().error_fg_color)); + ui.add_space(6.0); + } if app.ui.recent.is_empty() { ui.label(egui::RichText::new(tl!("Documents you open will show up here.")).color(t.text_dim)); } @@ -371,13 +376,30 @@ fn open_list(app: &mut WordApp, ui: &mut Ui) { ui.painter().text(pos2(r.min.x + 40.0, r.min.y + 13.0), Align2::LEFT_CENTER, &name, medium(13.0), t.text); ui.painter().text(pos2(r.min.x + 40.0, r.min.y + 29.0), Align2::LEFT_CENTER, &p, regular(11.0), t.text_dim); if resp.clicked() { - let _ = app.run("file.open", json!({"path": p})); - app.ui.backstage = false; - app.canvas.want_focus = true; + let failed = open_recent(app, &p).err(); + ui.ctx().data_mut(|d| d.insert_temp(open_error_id(), failed.unwrap_or_default())); } } } +fn open_error_id() -> egui::Id { + egui::Id::new("backstage_open_error") +} + +/// Drop the Recent list's open error once File is closed, so it doesn't come back next time. +pub(crate) fn forget_open_error(ctx: &egui::Context) { + ctx.data_mut(|d| d.remove::(open_error_id())); +} + +/// Open a file from the Recent list. A file that can't be opened (deleted, moved, unreadable) +/// leaves File open so the list can say why, rather than closing it on a greyed-out window. +fn open_recent(app: &mut WordApp, path: &str) -> Result<(), String> { + app.run("file.open", json!({"path": path}))?; + app.ui.backstage = false; + app.canvas.want_focus = true; + Ok(()) +} + fn open_page(app: &mut WordApp, ui: &mut Ui) { heading(ui, "Open"); if ui.button(egui::RichText::new(tl!("Browse…")).font(medium(14.0))).clicked() { @@ -529,6 +551,19 @@ mod tests { } } + /// #2938: opening a deleted file from Recent keeps File open and reports the error instead of + /// closing it on a greyed-out window. + #[test] + #[cfg(not(target_arch = "wasm32"))] + fn opening_a_missing_recent_file_keeps_file_open_with_the_error() { + let mut app = WordApp::new(wordcraft_engine::Session::new(wordcraft_doc::Document::new()), Services::default()); + app.ui.backstage = true; + let missing = std::env::temp_dir().join(format!("wordcraft-missing-{}.docx", std::process::id())); + let err = super::open_recent(&mut app, &missing.to_string_lossy()).unwrap_err(); + assert!(!err.is_empty()); + assert!(app.ui.backstage, "File stays open"); + } + /// #1873: the macOS back chevron sits beside the traffic lights windowed, at the plain /// control inset in full screen. #[test] diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index 220131df8..ed4b62366 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -2208,6 +2208,10 @@ impl WordApp { if self.session.view.focus_mode && self.dialog.is_none() && ctx.input_mut(|i| i.consume_key(egui::Modifiers::NONE, egui::Key::Escape)) { let _ = self.run("view.focus", json!({"value": false})); } + // A failed Recent open is news only until File closes (#2938). + if !self.ui.backstage { + backstage::forget_open_error(&ctx); + } if self.ui.backstage { backstage::show(self, ui); } else if !self.has_document() {