diff --git a/apps/printcraft/src/main.rs b/apps/printcraft/src/main.rs index aa1822c31..475ae55f2 100644 --- a/apps/printcraft/src/main.rs +++ b/apps/printcraft/src/main.rs @@ -71,7 +71,8 @@ fn main() -> eframe::Result { } // eframe would otherwise derive the settings folder from the app id: keep it under "PrintCraft". let persistence_path = eframe::storage_dir("PrintCraft").map(|d| d.join("app.ron")); - let native = eframe::NativeOptions { viewport, persistence_path, ..Default::default() }; + let mut native = eframe::NativeOptions { viewport, persistence_path, ..Default::default() }; + prefer_integrated_gpu(&mut native); eframe::run_native( "PrintCraft", native, @@ -125,3 +126,16 @@ fn write_control_file(path: &str, port: u16, token: &str) -> std::io::Result<()> } f.write_all(json.as_bytes()) } + +/// Draw on the integrated GPU unless `WGPU_POWER_PREF` says otherwise. A PDF viewer has no use +/// for a discrete GPU, and on hybrid-graphics laptops (NVIDIA Optimus) the discrete one can lose +/// or corrupt its memory across suspend and screen lock, leaving the window illegible (issue #8). +/// It also saves battery. Machines with one GPU are unaffected. +fn prefer_integrated_gpu(native: &mut eframe::NativeOptions) { + if std::env::var_os("WGPU_POWER_PREF").is_some() { + return; + } + if let eframe::egui_wgpu::WgpuSetup::CreateNew(setup) = &mut native.wgpu_options.wgpu_setup { + setup.power_preference = eframe::wgpu::PowerPreference::LowPower; + } +} diff --git a/crates/ui-egui/src/dialogs.rs b/crates/ui-egui/src/dialogs.rs index 9ddae4f4b..a9fe55729 100644 --- a/crates/ui-egui/src/dialogs.rs +++ b/crates/ui-egui/src/dialogs.rs @@ -1252,6 +1252,24 @@ fn draft_changes(app: &PrintCraftApp) -> Option> { Some(edits) } +/// The "Save changes?" prompt's keys (issue #8), read before anything else can take them: Enter +/// saves (the default button) and Escape cancels; Don't save takes ⌘D / Ctrl+D, the macOS +/// convention, or Alt+D / Alt+N, the mnemonics Windows and Linux desktops use for it. +pub(crate) fn save_prompt_key(ctx: &egui::Context) -> Option> { + use egui::{Key, Modifiers}; + ctx.input_mut(|i| { + if i.consume_key(Modifiers::NONE, Key::Enter) { + Some(Some(true)) + } else if i.consume_key(Modifiers::NONE, Key::Escape) { + Some(None) + } else if [(Modifiers::COMMAND, Key::D), (Modifiers::ALT, Key::D), (Modifiers::ALT, Key::N)].into_iter().any(|(m, k)| i.consume_key(m, k)) { + Some(Some(false)) + } else { + None + } + }) +} + /// "Save changes?" when closing a tab or quitting with unsaved edits. fn save_prompt(app: &mut PrintCraftApp, ctx: &egui::Context) { let Some(req) = app.close_request else { return }; diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index 2656575c5..99d1343cf 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -1061,6 +1061,15 @@ impl PrintCraftApp { fn shortcuts(&mut self, ctx: &egui::Context) { use egui::Key; + // "Save changes?" is modal: its keys are its own (⌘D is Don't save there, not Document + // properties; Escape cancels it rather than clearing a selection), and nothing may run + // underneath it. They are read here, before the canvas can consume them. + if self.close_request.is_some() { + if let Some(choice) = dialogs::save_prompt_key(ctx) { + self.resolve_close(ctx, choice); + } + return; + } self.registry_shortcuts(ctx); if self.full_screen && ctx.input(|i| i.key_pressed(Key::Escape)) { self.set_full_screen(ctx, false); diff --git a/crates/ui-egui/tests/editing.rs b/crates/ui-egui/tests/editing.rs index 885b3b817..f87e5a77f 100644 --- a/crates/ui-egui/tests/editing.rs +++ b/crates/ui-egui/tests/editing.rs @@ -212,6 +212,44 @@ fn closing_a_dirty_tab_can_save_first() { let _ = std::fs::remove_file(out); } +#[test] +fn the_save_prompt_answers_to_the_keyboard() { + // Issue #8: Enter saves; ⌘D / Ctrl+D, Alt+D and Alt+N don't save; Escape cancels. While the + // prompt is open, ⌘D is not Document properties. + for (m, key) in [(Modifiers::COMMAND, Key::D), (Modifiers::ALT, Key::D), (Modifiers::ALT, Key::N)] { + let mut h = organize(2); + h.get_by_label("Rotate clockwise").click(); + h.run_steps(3); + h.key_press_modifiers(Modifiers::COMMAND, Key::W); + h.run_steps(3); + h.get_by_label_contains("Save changes to “doc.pdf”"); + h.key_press_modifiers(m, key); + h.run_steps(3); + assert!(h.state().views.is_empty(), "{m:?}+{key:?} doesn't save and closes"); + assert!(h.state().dialog.is_none(), "{m:?}+{key:?} ran no command underneath"); + } + let mut h = organize(2); + h.get_by_label("Rotate clockwise").click(); + h.run_steps(3); + h.key_press_modifiers(Modifiers::COMMAND, Key::W); + h.run_steps(3); + h.key_press(Key::Escape); + h.run_steps(3); + assert_eq!(h.state().views.len(), 1, "Escape cancels"); + assert!(h.state().close_request.is_none()); + + let out = temp_path("enter-saves.pdf"); + h.state_mut().save_override = Some(out.to_string_lossy().into_owned()); + h.key_press_modifiers(Modifiers::COMMAND, Key::W); + h.run_steps(3); + h.key_press(Key::Enter); + h.run_steps(3); + assert!(h.state().views.is_empty(), "Enter saves and closes"); + let saved = printcraft_render::inspect(std::sync::Arc::new(std::fs::read(&out).unwrap()), None).unwrap(); + assert_eq!(saved.pages[0].rotation, 90); + let _ = std::fs::remove_file(out); +} + #[test] fn clean_tabs_close_without_asking() { let mut h = harness(1, |_| {});