//! Two small Help pages (W18): the mouse cheat sheet (Help › Mouse //! buttons…), generated from `Engine::mouse_hints()` for every mode so it //! cannot drift from the mouse-function indicator, and "What's new" //! (Help › What's new…), shown once after the version changes (the version //! last seen is kept in the preferences, `whats_new_seen`). use crate::platform::Os; use crate::App; use rustfig_engine::{Engine, ToolKind}; /// One mode's three buttons (and the Shift variant where the mode has one). #[derive(Debug, Clone, PartialEq, Eq)] pub struct MouseRow { pub id: &'static str, pub label: &'static str, pub left: &'static str, pub middle: &'static str, pub right: &'static str, pub shift: &'static str, } /// Every mode with the words of its mouse-function indicator. pub fn mouse_rows() -> Vec { ToolKind::ALL .iter() .map(|&t| { let mut e = Engine::new(); e.set_tool(t); let h = e.mouse_hints(); MouseRow { id: t.id(), label: t.label(), left: h.left, middle: h.middle, right: h.right, shift: h.shift, } }) .collect() } /// How to press the middle and right buttons without them, for `os`. pub fn substitutes(os: Os) -> Vec<&'static str> { let mut v = match os { Os::Mac => vec![ "Middle button: Option+click.", "Right button: Ctrl+click, or a two-finger click.", ], Os::Windows | Os::Linux => vec![ "Middle button: Alt+click, or the left and right buttons together (a chord, as on a two-button mouse).", "Right button: the right button (Ctrl+click is xfig's zoom).", ], }; v.push("Touch screens: a long press is the right button."); v } /// State of the mouse sheet. #[derive(Default)] pub struct MouseSheet { pub open: bool, rows: Vec, } impl MouseSheet { pub fn show(&mut self) { self.rows = mouse_rows(); self.open = true; } } /// Show the mouse sheet when it is open. pub fn show_mouse_sheet(app: &mut App, ctx: &egui::Context) { if !app.mouse_sheet.open { return; } let mut open = true; let os = Os::current(ctx); let rows = &app.mouse_sheet.rows; egui::Window::new("Mouse buttons") .open(&mut open) .default_size([640.0, 460.0]) .show(ctx, |ui| { for s in substitutes(os) { ui.label(s); } ui.separator(); egui::ScrollArea::vertical().show(ui, |ui| { egui::Grid::new("mouse_grid") .striped(true) .num_columns(5) .show(ui, |ui| { for h in ["Mode", "Left", "Middle", "Right", "With Shift"] { ui.strong(h); } ui.end_row(); for r in rows { ui.label(r.label); ui.label(r.left); ui.label(r.middle); ui.label(r.right); ui.label(r.shift); ui.end_row(); } }); }); }); app.mouse_sheet.open = open; } /// The "What's new" text: a few lines per release, newest first. pub const WHATS_NEW: &str = "\ Command search: press Ctrl+Shift+K (Cmd+Shift+K on macOS) to find any command or menu entry by name and run it. F1 over a mode button, the indicator panel or an open dialog opens the matching page of the manual. Help > Mouse buttons lists what the left, middle and right buttons do in every mode. Help > What's new shows this page again."; /// Whether the page is shown at start: when the version last seen is not /// the running one. Not on the first start (none stored): there is nothing /// new yet, and scratch settings folders would show it on every launch. pub fn should_show(seen: Option<&str>, current: &str) -> bool { seen.is_some_and(|s| s != current) } /// At start: open the page if the version changed and remember it, so it /// shows once per version. pub fn check_version(app: &mut App) { let current = env!("CARGO_PKG_VERSION"); let seen = app.engine.prefs.rustfig.whats_new_seen.as_deref(); if seen == Some(current) { return; } app.whats_new_open = should_show(seen, current); app.engine.prefs.rustfig.whats_new_seen = Some(current.to_string()); app.engine.prefs_dirty = true; } /// Show the "What's new" window when it is open. pub fn show_whats_new(app: &mut App, ctx: &egui::Context) { if !app.whats_new_open { return; } let mut open = true; egui::Window::new("What's new") .open(&mut open) .default_width(420.0) .show(ctx, |ui| { crate::dialog_body(ui, |ui| { ui.strong(format!("RustFig {}", env!("CARGO_PKG_VERSION"))); ui.separator(); for l in WHATS_NEW.lines() { ui.label(format!("\u{2022} {l}")); } }) }); app.whats_new_open = open; } #[cfg(test)] mod tests { use super::*; #[test] fn shows_once_per_version() { assert!(!should_show(None, "1.2.0")); assert!(should_show(Some("1.1.0"), "1.2.0")); assert!(!should_show(Some("1.2.0"), "1.2.0")); } #[test] fn check_version_marks_the_version_seen() { let mut app = App::with_engine(Engine::new()); check_version(&mut app); assert!(!app.whats_new_open, "not on the first start"); assert_eq!( app.engine.prefs.rustfig.whats_new_seen.as_deref(), Some(env!("CARGO_PKG_VERSION")) ); app.engine.prefs.rustfig.whats_new_seen = Some("0.0.1".into()); check_version(&mut app); assert!(app.whats_new_open); app.whats_new_open = false; check_version(&mut app); assert!(!app.whats_new_open); } #[test] fn sheet_covers_every_mode_and_follows_the_indicator() { let rows = mouse_rows(); assert_eq!(rows.len(), ToolKind::ALL.len()); for t in ToolKind::ALL { let r = rows.iter().find(|r| r.id == t.id()).unwrap(); let mut e = Engine::new(); e.set_tool(t); let h = e.mouse_hints(); assert_eq!((r.left, r.middle, r.right), (h.left, h.middle, h.right), "{}", t.id()); assert!( !(r.left.is_empty() && r.middle.is_empty() && r.right.is_empty()), "{}", t.id() ); } } #[test] fn substitutes_per_system() { assert!(substitutes(Os::Mac).iter().any(|s| s.contains("Option+click"))); assert!(substitutes(Os::Mac).iter().any(|s| s.contains("Ctrl+click"))); for os in [Os::Windows, Os::Linux] { let v = substitutes(os).join(" "); assert!(v.contains("Alt+click") && v.contains("together")); } for os in [Os::Mac, Os::Windows, Os::Linux] { assert!(substitutes(os).iter().any(|s| s.contains("long press"))); } } } .