From 45b8dce220afd86f688fb2d184a29880e0acc162 Mon Sep 17 00:00:00 2001 From: MarekSpodBiedry <73122968+MarekSpodBiedry@users.noreply.github.com> Date: Tue, 13 Oct 2026 17:33:35 +0200 Subject: [PATCH] Info: keep a property draft when another field or the side menu is clicked (#422) --- crates/ui-egui/src/backstage.rs | 29 +++++++++++++++++--- crates/ui-egui/src/backstage_tests.rs | 38 +++++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 4 deletions(-) diff --git a/crates/ui-egui/src/backstage.rs b/crates/ui-egui/src/backstage.rs index f1d0fde3..2d2dba2a 100644 --- a/crates/ui-egui/src/backstage.rs +++ b/crates/ui-egui/src/backstage.rs @@ -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 { diff --git a/crates/ui-egui/src/backstage_tests.rs b/crates/ui-egui/src/backstage_tests.rs index 115d782f..55871074 100644 --- a/crates/ui-egui/src/backstage_tests.rs +++ b/crates/ui-egui/src/backstage_tests.rs @@ -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]