diff --git a/crates/ui-egui/src/dialogs.rs b/crates/ui-egui/src/dialogs.rs index 18d112c8..abe8c87e 100644 --- a/crates/ui-egui/src/dialogs.rs +++ b/crates/ui-egui/src/dialogs.rs @@ -108,6 +108,9 @@ pub fn show(app: &mut SlideApp, ctx: &egui::Context) { let escape = ctx.input(|i| i.key_pressed(egui::Key::Escape)); if open && !close && !escape { app.dialog = Some(d); + } else if !close && d.id == "preferences" { + // Esc or the window's close button: the same as Cancel. + restore_preferences(app, &d); } else if app.dialog.is_none() && let Some(ret) = d.params.get("returnParams").filter(|v| v.is_object()).cloned() { @@ -137,6 +140,31 @@ fn buttons(ui: &mut Ui, ok_label: &str) -> (bool, bool) { (ok, cancel) } +/// What the Preferences dialog can change, as it was when the dialog opened. +fn preferences_snapshot(app: &SlideApp) -> Value { + json!({ + "dark": app.ui.brightness == theme::Brightness::Dark, + "uiScale": app.ui.ui_scale, + "prefs": serde_json::to_value(&app.session.prefs).unwrap_or(Value::Null), + }) +} + +/// Puts back the settings the Preferences dialog opened with (Cancel, Esc, the close button). +fn restore_preferences(app: &mut SlideApp, d: &Dialog) { + let Some(before) = d.params.get("before") else { return }; + if let Some(dark) = before.get("dark").and_then(Value::as_bool) + && dark != (app.ui.brightness == theme::Brightness::Dark) + { + let _ = app.run("view.dark", json!({ "on": dark })); + } + if let Some(scale) = before.get("uiScale").and_then(Value::as_f64) { + app.ui.ui_scale = scale as f32; + } + if let Some(prefs) = before.get("prefs").and_then(|p| serde_json::from_value(p.clone()).ok()) { + app.session.prefs = prefs; + } +} + fn field(ui: &mut Ui, d: &mut Dialog, label: &str, key: &str, default: &str) { let mut v = d.get(key, default); ui.horizontal(|ui| { @@ -2104,6 +2132,11 @@ fn body(app: &mut SlideApp, ui: &mut Ui, d: &mut Dialog) -> bool { } "gridOptions" => grid_options(app, ui, d), "preferences" => { + // The controls apply as they change, so the dialog remembers what it opened with and + // Cancel (or Esc, or the close button: see `show`) puts that back. + if d.params.get("before").is_none() { + d.params = json!({ "before": preferences_snapshot(app) }); + } ui.label(egui::RichText::new("General").font(theme::bold(13.0))); let mut dark = app.ui.brightness == theme::Brightness::Dark; if ui.checkbox(&mut dark, "Dark appearance").changed() { @@ -2130,8 +2163,11 @@ fn body(app: &mut SlideApp, ui: &mut Ui, d: &mut Dialog) -> bool { ui.label("Your name (comments):"); ui.text_edit_singleline(&mut app.session.prefs.author); }); - let (ok, _) = buttons(ui, "Done"); - ok + let (ok, cancel) = buttons(ui, "Done"); + if cancel { + restore_preferences(app, d); + } + ok || cancel } "photoAlbum" => photo_album_dialog(app, ui, d), "themeColors" => theme_colors_dialog(app, ui, d), diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index f020f5f5..9d4a681b 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -2573,6 +2573,71 @@ mod tests { assert!((harness.state().session.prefs.grid_spacing - 6.0).abs() < 1e-9); } + /// Preferences (#752): Cancel, Esc and the window's close button put back what the dialog + /// changed while it was open; Done keeps it. + #[test] + fn preferences_cancel_restores_the_settings_done_keeps_them() { + use egui_kittest::kittest::Queryable; + let mut harness = egui_kittest::Harness::builder().with_size(egui::vec2(1200.0, 800.0)).build_ui_state( + |ui, app: &mut SlideApp| { + app.logic(ui.ctx()); + app.ui(ui); + }, + SlideApp::new(Session::with_new(), Services::default()), + ); + harness.run_steps(2); + let before = harness.state().session.prefs.clone(); + let dark_before = harness.state().ui.brightness; + let change = |harness: &mut egui_kittest::Harness<'static, SlideApp>| { + harness.state_mut().run("app.preferences", json!({})).unwrap(); + harness.run_steps(2); + for label in ["Dark appearance", "Show smart guides", "Snap objects to grid"] { + harness + .query_all_by_role_and_label(egui::accesskit::Role::CheckBox, label) + .next() + .unwrap_or_else(|| panic!("no {label} checkbox")) + .click_accesskit(); + harness.run_steps(2); + } + assert_ne!(harness.state().ui.brightness, dark_before, "the dialog previews the appearance"); + assert_ne!(harness.state().session.prefs.smart_guides, before.smart_guides); + }; + let unchanged = |harness: &egui_kittest::Harness<'static, SlideApp>, how: &str| { + let s = harness.state(); + assert!(s.dialog.is_none(), "{how} closes Preferences"); + assert_eq!(s.ui.brightness, dark_before, "{how} restores the appearance"); + assert_eq!(s.session.prefs.smart_guides, before.smart_guides, "{how} restores smart guides"); + assert_eq!(s.session.prefs.snap_to_grid, before.snap_to_grid, "{how} restores snap to grid"); + }; + + change(&mut harness); + harness + .query_all_by_role_and_label(egui::accesskit::Role::Button, "Cancel") + .next() + .unwrap_or_else(|| panic!("no Cancel button")) + .click_accesskit(); + harness.run_steps(2); + unchanged(&harness, "Cancel"); + + change(&mut harness); + harness.key_press(egui::Key::Escape); + harness.run_steps(2); + unchanged(&harness, "Esc"); + + change(&mut harness); + harness + .query_all_by_role_and_label(egui::accesskit::Role::Button, "Done") + .next() + .unwrap_or_else(|| panic!("no Done button")) + .click_accesskit(); + harness.run_steps(2); + let s = harness.state(); + assert!(s.dialog.is_none(), "Done closes Preferences"); + assert_ne!(s.ui.brightness, dark_before, "Done keeps the appearance"); + assert_ne!(s.session.prefs.smart_guides, before.smart_guides, "Done keeps smart guides"); + assert_ne!(s.session.prefs.snap_to_grid, before.snap_to_grid, "Done keeps snap to grid"); + } + fn canvas_harness() -> egui_kittest::Harness<'static, SlideApp> { let mut harness = egui_kittest::Harness::builder().with_size(egui::vec2(1200.0, 800.0)).build_ui_state( |ui, app: &mut SlideApp| {