mirror of
https://github.com/storytold/wordcraft.git
synced 2026-11-14 14:32:31 +00:00
Backstage: add Save As page and close on successful save (#727)
* Backstage: add Save As page and close on successful save * Review fixes for #727: Save As page without the open list; unnamed Save opens no native dialog The Save As page reused the Open page's recent list, so clicking a document there opened it. It now has only Browse…. Save on a document without a file goes to the Save As page instead of running file.save, which also opened the native Save As dialog. Test for both. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Print-page test clicks the page's Print button (the backstage list now has a Print entry too) --------- Co-authored-by: Brandon Thomas <echelon@gmail.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
3 files changed
+146
-7
No files matched your search
@@ -59,6 +59,7 @@ pub fn show(app: &mut WordApp, ui: &mut Ui) {
|
||||
ui.add_space(if mac { 12.0 } else { 6.0 });
|
||||
for (id, label) in PAGES {
|
||||
let (r, resp) = ui.allocate_exact_size(vec2(200.0, 38.0), Sense::click());
|
||||
resp.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, ui.is_enabled(), tl!(label)));
|
||||
let active = app.ui.backstage_page == id;
|
||||
if active {
|
||||
ui.painter().rect_filled(r, 0.0, egui::Color32::from_white_alpha(46));
|
||||
@@ -74,14 +75,23 @@ pub fn show(app: &mut WordApp, ui: &mut Ui) {
|
||||
);
|
||||
if resp.clicked() {
|
||||
match id {
|
||||
// A document without a file goes to the Save As page (as in Word) instead
|
||||
// of `file.save`, which would also open the native Save As dialog.
|
||||
// In the browser a save is a download, so it saves right away.
|
||||
"save" if app.session.path.is_none() && app.services.download.is_none() => {
|
||||
app.ui.backstage_page = "saveAs".into();
|
||||
}
|
||||
"save" => {
|
||||
let _ = app.run("file.save", json!({}));
|
||||
let r = app.run("file.save", json!({}));
|
||||
if r.is_ok_and(|v| v.get("saved").and_then(serde_json::Value::as_bool) == Some(true) || v.get("downloaded").is_some()) {
|
||||
app.ui.backstage = false;
|
||||
app.canvas.want_focus = true;
|
||||
} else {
|
||||
app.ui.backstage_page = "saveAs".into();
|
||||
}
|
||||
}
|
||||
"saveAs" => {
|
||||
app.save_as_dialog();
|
||||
}
|
||||
"open" => {
|
||||
app.ui.backstage_page = id.into();
|
||||
app.ui.backstage_page = "saveAs".into();
|
||||
}
|
||||
_ => app.ui.backstage_page = id.into(),
|
||||
}
|
||||
@@ -90,6 +100,7 @@ pub fn show(app: &mut WordApp, ui: &mut Ui) {
|
||||
ui.with_layout(egui::Layout::bottom_up(egui::Align::Min), |ui| {
|
||||
for (label, id) in [("About", "about"), ("Discord community", "discord")] {
|
||||
let (r, resp) = ui.allocate_exact_size(vec2(200.0, 32.0), Sense::click());
|
||||
resp.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, ui.is_enabled(), tl!(label)));
|
||||
if resp.hovered() {
|
||||
ui.painter().rect_filled(r, 0.0, egui::Color32::from_white_alpha(26));
|
||||
}
|
||||
@@ -109,6 +120,7 @@ pub fn show(app: &mut WordApp, ui: &mut Ui) {
|
||||
"home" => home_page(app, ui),
|
||||
"new" => new_page(app, ui),
|
||||
"open" => open_page(app, ui),
|
||||
"saveAs" => save_as_page(app, ui),
|
||||
"info" => info_page(app, ui),
|
||||
"print" | "export" => export_page(app, ui),
|
||||
"options" => crate::options::page(app, ui),
|
||||
@@ -277,6 +289,15 @@ fn open_page(app: &mut WordApp, ui: &mut Ui) {
|
||||
open_list(app, ui);
|
||||
}
|
||||
|
||||
/// Save As: Browse… asks where to save. It has no recent-documents list: clicking one of those
|
||||
/// opens it, which is not what Save As is for.
|
||||
fn save_as_page(app: &mut WordApp, ui: &mut Ui) {
|
||||
heading(ui, "Save As");
|
||||
if ui.button(egui::RichText::new(tl!("Browse…")).font(medium(14.0))).clicked() {
|
||||
app.save_as_dialog();
|
||||
}
|
||||
}
|
||||
|
||||
fn info_page(app: &mut WordApp, ui: &mut Ui) {
|
||||
heading(ui, "Info");
|
||||
let info = app.session.run("file.info", &json!({})).unwrap_or_default();
|
||||
@@ -469,4 +490,115 @@ mod tests {
|
||||
a.session.run("edit.undo", &json!({})).unwrap();
|
||||
assert_eq!(a.session.doc.core.title, "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn save_as_page_shows_browse_and_no_open_list() {
|
||||
let mut a = WordApp::new(wordcraft_engine::Session::new(wordcraft_doc::Document::new()), Services::default());
|
||||
a.run("ui.backstage", json!({"value": true, "page": "saveAs"})).unwrap();
|
||||
let mut h = egui_kittest::Harness::builder().with_size(egui::vec2(1440.0, 900.0)).build_ui_state(
|
||||
|ui, app: &mut WordApp| {
|
||||
let ctx = ui.ctx().clone();
|
||||
app.logic(&ctx);
|
||||
app.ui(ui);
|
||||
},
|
||||
a,
|
||||
);
|
||||
h.run_steps(4);
|
||||
assert!(h.query_by_label("Browse…").is_some(), "Save As has Browse button");
|
||||
assert!(h.query_by_label("Recent").is_none(), "Save As has no list that opens documents");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn backstage_save_navigates_to_save_as_when_unnamed() {
|
||||
let mut a = WordApp::new(wordcraft_engine::Session::new(wordcraft_doc::Document::new()), Services::default());
|
||||
a.run("ui.backstage", json!({"value": true, "page": "home"})).unwrap();
|
||||
let mut h = egui_kittest::Harness::builder().with_size(egui::vec2(1440.0, 900.0)).build_ui_state(
|
||||
|ui, app: &mut WordApp| {
|
||||
let ctx = ui.ctx().clone();
|
||||
app.logic(&ctx);
|
||||
app.ui(ui);
|
||||
},
|
||||
a,
|
||||
);
|
||||
h.run_steps(4);
|
||||
h.get_by_label("Save").click();
|
||||
h.run_steps(4);
|
||||
assert_eq!(h.state().ui.backstage_page, "saveAs", "unnamed Save opens Save As page");
|
||||
assert!(h.state().ui.backstage, "backstage remains open");
|
||||
}
|
||||
|
||||
/// Save on a document without a file shows the Save As page only: no native dialog as well.
|
||||
#[test]
|
||||
fn backstage_save_when_unnamed_opens_no_file_dialog() {
|
||||
let shown = std::rc::Rc::new(std::cell::Cell::new(0));
|
||||
let count = shown.clone();
|
||||
let services = Services {
|
||||
file_dialog: Some(Box::new(move |_| {
|
||||
count.set(count.get() + 1);
|
||||
std::sync::mpsc::channel::<crate::FileDialogAnswer>().1
|
||||
})),
|
||||
..Default::default()
|
||||
};
|
||||
let mut a = WordApp::new(wordcraft_engine::Session::new(wordcraft_doc::Document::new()), services);
|
||||
a.run("ui.backstage", json!({"value": true, "page": "home"})).unwrap();
|
||||
let mut h = egui_kittest::Harness::builder().with_size(egui::vec2(1440.0, 900.0)).build_ui_state(
|
||||
|ui, app: &mut WordApp| {
|
||||
let ctx = ui.ctx().clone();
|
||||
app.logic(&ctx);
|
||||
app.ui(ui);
|
||||
},
|
||||
a,
|
||||
);
|
||||
h.run_steps(4);
|
||||
h.get_by_label("Save").click();
|
||||
h.run_steps(4);
|
||||
assert_eq!(h.state().ui.backstage_page, "saveAs");
|
||||
assert_eq!(shown.get(), 0, "no native Save As dialog");
|
||||
assert!(!h.state().file_dialog_open());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn backstage_save_closes_when_saved() {
|
||||
let got: std::rc::Rc<std::cell::RefCell<Vec<(String, Vec<u8>)>>> = Default::default();
|
||||
let sink = got.clone();
|
||||
let services = Services {
|
||||
download: Some(Box::new(move |n: &str, b: &[u8]| {
|
||||
sink.borrow_mut().push((n.to_string(), b.to_vec()));
|
||||
Ok(())
|
||||
})),
|
||||
..Default::default()
|
||||
};
|
||||
let mut a = WordApp::new(wordcraft_engine::Session::new(wordcraft_doc::Document::new()), services);
|
||||
a.run("ui.backstage", json!({"value": true, "page": "home"})).unwrap();
|
||||
let mut h = egui_kittest::Harness::builder().with_size(egui::vec2(1440.0, 900.0)).build_ui_state(
|
||||
|ui, app: &mut WordApp| {
|
||||
let ctx = ui.ctx().clone();
|
||||
app.logic(&ctx);
|
||||
app.ui(ui);
|
||||
},
|
||||
a,
|
||||
);
|
||||
h.run_steps(4);
|
||||
h.get_by_label("Save").click();
|
||||
h.run_steps(4);
|
||||
assert!(!h.state().ui.backstage, "successful save closes backstage");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn backstage_save_as_sidebar_opens_save_as_page() {
|
||||
let mut a = WordApp::new(wordcraft_engine::Session::new(wordcraft_doc::Document::new()), Services::default());
|
||||
a.run("ui.backstage", json!({"value": true, "page": "home"})).unwrap();
|
||||
let mut h = egui_kittest::Harness::builder().with_size(egui::vec2(1440.0, 900.0)).build_ui_state(
|
||||
|ui, app: &mut WordApp| {
|
||||
let ctx = ui.ctx().clone();
|
||||
app.logic(&ctx);
|
||||
app.ui(ui);
|
||||
},
|
||||
a,
|
||||
);
|
||||
h.run_steps(4);
|
||||
h.get_by_label("Save As").click();
|
||||
h.run_steps(4);
|
||||
assert_eq!(h.state().ui.backstage_page, "saveAs");
|
||||
}
|
||||
}
|
||||
@@ -233,8 +233,13 @@ impl WordApp {
|
||||
}
|
||||
AfterPick::SaveAs(after) => {
|
||||
let saved = picked.is_some_and(|path| {
|
||||
self.run("file.save", json!({"path": path})).is_ok_and(|v| v.get("saved").and_then(Value::as_bool) == Some(true))
|
||||
self.run("file.save", json!({"path": path}))
|
||||
.is_ok_and(|v| v.get("saved").and_then(Value::as_bool) == Some(true) || v.get("downloaded").is_some())
|
||||
});
|
||||
if saved {
|
||||
self.ui.backstage = false;
|
||||
self.canvas.want_focus = true;
|
||||
}
|
||||
match after {
|
||||
AfterSave::Nothing => Ok(json!({"saved": saved})),
|
||||
AfterSave::AutoSave => {
|
||||
|
||||
@@ -2539,7 +2539,9 @@ mod tests {
|
||||
assert_eq!(h.query_by_label(BLURB).is_some(), hook, "hook {hook}: the Print button's blurb");
|
||||
assert!(h.query_by_label("PDF document (*.pdf)").is_some(), "hook {hook}: the page rendered its export choices");
|
||||
if hook {
|
||||
h.get_by_role_and_label(egui::accesskit::Role::Button, "Print").click();
|
||||
// The page's own Print button (the backstage list has a Print entry too): the last.
|
||||
let buttons: Vec<_> = h.query_all(egui_kittest::kittest::By::new().role(egui::accesskit::Role::Button).label("Print")).collect();
|
||||
buttons.last().expect("a Print button").click();
|
||||
h.step();
|
||||
h.step();
|
||||
assert_eq!(printed.get(), 1, "the button printed");
|
||||
|
||||
Reference in new issue
Block a user