Merge PR #445 from MarekSpodBiedry: Info: keep a property draft when another field or the side menu is clicked (#422)

This commit is contained in:
Brandon Thomas
2026-10-14 12:53:32 -04:00
2 changed files with 63 additions and 4 deletions
+25 -4
View File
@@ -353,6 +353,7 @@ pub fn show(app: &mut SlideApp, ui: &mut Ui) {
}
if nav_item(ui, *nav, &bs, index, &t).clicked() {
bs.focus = None;
commit_props(app, &bs);
match act(app, *nav, bs.page) {
None => out.close = true,
Some(p) => out.page = Some(p),
@@ -367,6 +368,7 @@ pub fn show(app: &mut SlideApp, ui: &mut Ui) {
for (k, nav) in bottom.iter().enumerate().rev() {
if nav_item(ui, *nav, &bs, first_bottom + k, &t).clicked() {
bs.focus = None;
commit_props(app, &bs);
match act(app, *nav, bs.page) {
None => out.close = true,
Some(p) => out.page = Some(p),
@@ -796,15 +798,16 @@ fn info(app: &mut SlideApp, ui: &mut Ui, bs: &mut Backstage) {
let Some(draft) = bs.props.get_mut(i) else { continue };
let l = ui.label(RichText::new(*label).color(t.text_dim));
let id = ui.make_persistent_id(("backstage_prop", *key));
// Show the presentation's value unless the field is being edited (undo…).
if !ui.ctx().memory(|m| m.has_focus(id)) {
draft.clone_from(&saved);
}
let resp = ui.add(egui::TextEdit::singleline(draft).id(id).hint_text(*hint).desired_width(300.0)).labelled_by(l.id);
if resp.lost_focus() && *draft != saved {
// Commit first: a click on another field takes the focus before this one is drawn.
let mut p = serde_json::Map::new();
p.insert((*key).to_string(), json!(draft.clone()));
let _ = app.run("file.properties", Value::Object(p));
} else if !resp.has_focus() && *draft != saved {
// Show the presentation's value unless the field is being edited (undo...).
draft.clone_from(&saved);
ui.ctx().request_repaint();
}
ui.end_row();
}
@@ -814,6 +817,24 @@ fn info(app: &mut SlideApp, ui: &mut Ui, bs: &mut Backstage) {
});
}
/// Commits what the Info page's property fields hold and the presentation does not: a side-menu
/// action such as Save runs before the field that is being left is drawn again (#422).
fn commit_props(app: &mut SlideApp, bs: &Backstage) {
if bs.page != Page::Info {
return;
}
let Ok(v) = app.session.execute("file.info", &json!({})) else { return };
let mut p = serde_json::Map::new();
for ((key, _, _), draft) in PROPERTIES.iter().zip(&bs.props) {
if v["properties"].get(*key).and_then(Value::as_str).unwrap_or("") != *draft {
p.insert((*key).to_string(), json!(draft));
}
}
if !p.is_empty() {
let _ = app.run("file.properties", Value::Object(p));
}
}
/// `1234567` → `1.2 MB`.
pub fn format_bytes(b: u64) -> String {
match b {
+38
View File
@@ -347,6 +347,44 @@ fn info_shows_and_edits_the_properties() {
assert!(h.state().backstage_open());
}
/// Leaving a property field with a click on another field keeps what was typed.
#[test]
fn info_keeps_a_property_draft_when_another_field_is_clicked() {
let mut h = harness(new_app());
h.state_mut().run("app.backstage", json!({"page": "info"})).unwrap();
h.run_steps(3);
h.get_by_role_and_label(Role::TextInput, "Author").click();
h.run_steps(2);
h.get_by_role_and_label(Role::TextInput, "Author").type_text("Ada");
h.run_steps(2);
h.get_by_role_and_label(Role::TextInput, "Title").click();
h.run_steps(4);
assert_eq!(h.state().session.active().unwrap().doc.props.author, "Ada");
}
/// Save straight from a property field saves what was typed (the side menu acts before the field
/// is drawn again).
#[test]
fn info_saves_a_property_draft_when_save_is_clicked() {
let dir = temp_dir("info-save");
let path = dir.join("Draft.pptx").to_string_lossy().to_string();
let mut s = Session::with_new();
s.execute("file.saveAs", &json!({"path": path})).unwrap();
let mut h = harness(SlideApp::new(s, Services::default()));
h.state_mut().run("app.backstage", json!({"page": "info"})).unwrap();
h.run_steps(3);
h.get_by_role_and_label(Role::TextInput, "Author").click();
h.run_steps(2);
h.get_by_role_and_label(Role::TextInput, "Author").type_text("Ada");
h.run_steps(2);
click_nav(&h, "Save");
h.run_steps(3);
let doc = h.state().session.active().unwrap();
assert_eq!(doc.doc.props.author, "Ada");
assert!(!doc.is_dirty(), "the author went into the saved file");
let _ = std::fs::remove_dir_all(&dir);
}
/// With nothing open the File tab is the start screen: Home, New and Open, no back arrow, and Esc
/// has nowhere to go.
#[test]