W1: every way of opening a file goes through WordApp::open_file

This commit is contained in:
aiongg
2026-10-24 04:12:56 +00:00
parent 00856d7178
commit 6f7da7345f
7 changed files with 92 additions and 12 deletions
+1 -1
View File
@@ -43,7 +43,7 @@ pub fn poll(inbox: &Inbox, app: &mut WordApp, ctx: &egui::Context) {
match e {
Event::Open(paths) => {
for p in paths.iter().filter_map(|p| p.to_str()) {
match app.run("file.open", serde_json::json!({"path": p})) {
match app.open_file(p, None) {
// Leave Backstage, as opening from the Open dialog does.
Ok(_) => app.ui.backstage = false,
Err(e) => {
+2 -2
View File
@@ -564,8 +564,8 @@ pub(crate) fn forget_save_as_error(ctx: &egui::Context) {
/// 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}))?;
pub(crate) fn open_recent(app: &mut WordApp, path: &str) -> Result<(), String> {
app.open_file(path, None)?;
app.ui.backstage = false;
app.canvas.want_focus = true;
Ok(())
+16
View File
@@ -102,6 +102,22 @@ impl WordApp {
seen
}
/// Open the file at `path` for the window showing document `into` (`None`: the active
/// document's window). Every way of opening a file comes through here, so each follows the
/// same rule: an untouched blank document is replaced in its own window, else the file opens
/// in a window of its own (or its window comes forward if it is already open). A file a
/// dialog picked arrives while any window draws, so the document it was asked from is made
/// active again first.
pub fn open_file(&mut self, path: &str, into: Option<u64>) -> Result<Value, String> {
if let Some(id) = into
&& id != self.session.document_id()
&& self.session.is_open(id)
{
self.show_document(id)?;
}
self.run("file.open", json!({"path": path}))
}
/// Show open document `id` in the window (View › Window › Switch Windows, quitting).
pub(crate) fn show_document(&mut self, id: u64) -> Result<(), String> {
self.session.activate(id).map_err(|e| e.to_string())?;
+4 -3
View File
@@ -286,7 +286,7 @@ impl WordApp {
FileDialogRequest::Save { name: self.default_save_name(), dir: None },
AfterPick::SaveAs { after: crate::file_dialogs::AfterSave::Nothing, document: self.session.document_id() },
),
_ => (FileDialogRequest::Open { purpose: "document".into() }, AfterPick::OpenDocument),
_ => (FileDialogRequest::Open { purpose: "document".into() }, AfterPick::OpenDocument { document: self.session.document_id() }),
};
self.show_file_browser(req, after, false);
}
@@ -529,7 +529,7 @@ mod tests {
std::fs::create_dir(dir.join("drafts")).unwrap();
std::fs::write(dir.join("drafts").join("old.docx"), b"x").unwrap();
let req = FileDialogRequest::Save { name: "Document1.docx".into(), dir: None };
let mut b = FileBrowser::new(req, AfterPick::OpenDocument, dir.clone(), false);
let mut b = FileBrowser::new(req, AfterPick::OpenDocument { document: 0 }, dir.clone(), false);
b.name = "drafts".into();
assert_eq!(b.accept(), Step::Stay);
assert_eq!(b.dir, dir.join("drafts"));
@@ -577,7 +577,8 @@ mod tests {
fn an_empty_folder_says_there_are_no_items() {
let dir = scratch("empty_state");
let mut a = WordApp::new(Session::new(wordcraft_doc::Document::new()), Services::default());
let mut b = FileBrowser::new(FileDialogRequest::Open { purpose: "document".into() }, AfterPick::OpenDocument, dir.clone(), false);
let mut b =
FileBrowser::new(FileDialogRequest::Open { purpose: "document".into() }, AfterPick::OpenDocument { document: 0 }, dir.clone(), false);
b.navigate(dir.clone());
assert!(matches!(&b.listing, Ok(l) if l.entries.is_empty()), "the folder starts empty");
a.file_browser.open = Some(Box::new(b));
+6 -5
View File
@@ -56,8 +56,8 @@ pub fn open_filters(purpose: &str) -> &'static [(&'static str, &'static [&'stati
/// What to do with the path picked in a dialog.
pub(crate) enum AfterPick {
/// Open the picked document.
OpenDocument,
/// Open the picked document from the window showing `document` ([`WordApp::open_file`]).
OpenDocument { document: u64 },
/// Insert the picked picture, or replace the pending Change Picture target.
Picture,
/// Save the document there, then carry on.
@@ -242,9 +242,9 @@ impl WordApp {
/// Do what the dialog was for with the picked path (`None`: the user cancelled).
pub(crate) fn finish_pick(&mut self, after: AfterPick, picked: Option<String>) -> Result<Value, String> {
match after {
AfterPick::OpenDocument => {
AfterPick::OpenDocument { document } => {
let Some(path) = picked else { return Ok(json!({"cancelled": true})) };
let r = self.run("file.open", json!({"path": path}));
let r = self.open_file(&path, Some(document));
self.ui.backstage = false;
r
}
@@ -450,7 +450,8 @@ impl WordApp {
f("document");
return;
}
let _ = self.ask_file(FileDialogRequest::Open { purpose: "document".into() }, AfterPick::OpenDocument);
let _ =
self.ask_file(FileDialogRequest::Open { purpose: "document".into() }, AfterPick::OpenDocument { document: self.session.document_id() });
}
pub(crate) fn pick_picture(&mut self) {
+1 -1
View File
@@ -2479,7 +2479,7 @@ impl WordApp {
} else if wordcraft_fonts::is_font_file_name(&lp) {
self.run("font.add", json!({"path": path}))
} else {
self.run("file.open", json!({"path": path}))
self.open_file(&path, None)
};
}
}
+62
View File
@@ -1251,4 +1251,66 @@ mod tests {
a.run("ui.saveChanges", json!({"answer": "dontSave"})).unwrap();
assert!(a.quit_requested, "one prompt for the document");
}
/// Two windows, the second (focused) on an untouched blank document: an app whose host
/// answers file dialogs only when the test does, plus the dialog's answer channel.
fn blank_beside_a_document() -> (egui::Context, WordApp, Shown, u64) {
let shown: Shown = std::rc::Rc::default();
let log = shown.clone();
let services = Services {
file_dialog: Some(Box::new(move |_| {
let (tx, rx) = std::sync::mpsc::channel();
log.borrow_mut().push(tx);
rx
})),
..Default::default()
};
let ctx = backend();
let mut a = WordApp::new(Session::new(wordcraft_doc::Document::new()), services);
frames(&ctx, &mut a, 2);
a.run("file.open", json!({"path": docx_on_disk("first", "First").to_string_lossy()})).unwrap();
a.run("file.new", json!({})).unwrap();
frames(&ctx, &mut a, 2);
assert!(a.window_count() == 2 && a.session.is_untouched_blank());
let blank = a.windows.shown();
(ctx, a, shown, blank)
}
type Shown = std::rc::Rc<RefCell<Vec<std::sync::mpsc::Sender<crate::file_dialogs::FileDialogAnswer>>>>;
/// Each way of opening a file replaces an untouched blank document in its own window, even
/// when the file arrives while another window draws: File › Open, ⌘O and the File page's
/// Browse (an asynchronous dialog), the File page's and Start screen's recent list, and
/// Finder (between frames, in the focused window).
#[test]
fn every_open_replaces_an_untouched_blank_document_in_its_window() {
let second = docx_on_disk("second", "Second");
let path = second.to_string_lossy().to_string();
let opens: [(&str, fn(&mut WordApp, &egui::Context, &Shown, &str)); 3] = [
("dialog", |a, ctx, shown, path| {
a.run("ui.backstage", json!({"value": true, "page": "open"})).unwrap();
a.run("ui.openFileDialog", json!({})).unwrap();
frames(ctx, a, 2);
let tx = shown.borrow_mut().pop().unwrap();
tx.send(Ok(vec![path.to_string()])).unwrap();
frames(ctx, a, 3);
}),
("recent", |a, ctx, _, path| {
crate::backstage::open_recent(a, path).unwrap();
frames(ctx, a, 2);
}),
("finder", |a, ctx, _, path| {
a.open_file(path, None).unwrap();
frames(ctx, a, 2);
}),
];
for (how, open) in opens {
let (ctx, mut a, shown, blank) = blank_beside_a_document();
open(&mut a, &ctx, &shown, &path);
assert_eq!(a.window_count(), 2, "{how}: no third window");
assert!(a.enter_window(blank), "{how}: the blank document's window is still open");
assert_eq!(text(&a), "Second", "{how}: it shows the opened file");
}
std::fs::remove_dir_all(second.parent().unwrap()).ok();
}
}