//! Context help (W18): F1 while the pointer is over a mode button, the //! indicator panel (attribute buttons) or the depth panel, or with a dialog //! open, opens the matching topic of the bundled manual; elsewhere F1 opens //! the manual's first page. Buttons note that they are hovered while they //! are drawn; F1 reads that note of the last frame. use crate::help::TOPICS; use crate::App; use rustfig_engine::ToolKind; use serde_json::json; /// What the last frame's widgets told about the pointer. #[derive(Default)] pub struct ContextHelp { /// Topic file of the widget under the pointer (set while drawing). pub hovered: Option<&'static str>, /// Panels: where they are and their topic. pub zones: Vec<(egui::Rect, &'static str)>, } /// The manual topic (file name) of a mode: the page with the mode's section. pub fn mode_topic(t: ToolKind) -> Option<&'static str> { let heading = format!("## {}\n", t.label()); TOPICS.iter().find(|x| x.text.contains(&heading)).map(|x| x.file) } /// Topic of the library button. pub const LIBRARY_TOPIC: &str = "libraries.md"; /// Topic of the indicator panel's attribute buttons. pub const INDICATOR_TOPIC: &str = "attributes.md"; /// Topic of the depth manager (right panel). pub const DEPTH_TOPIC: &str = "window.md"; /// Topic of the rulers and the units box. pub const RULER_TOPIC: &str = "window.md"; /// The topic of the open dialog (the first that is open), if any. pub fn dialog_topic(app: &App) -> Option<&'static str> { let d = &app.dialogs; let table: [(bool, &'static str); 12] = [ (d.export.open, "export.md"), (d.print.open, "print.md"), (d.search.open, "select-move.md"), (d.settings_open, "files.md"), (d.global_open, "preferences.md"), (d.versions_open, "files.md"), (d.color.open, "attributes.md"), (app.styles.open, "attributes.md"), (app.unit_panel_open, "preferences.md"), (app.keys_sheet.open, "keys.md"), (app.mouse_sheet.open, "mouse.md"), (app.library.open, "libraries.md"), ]; table.iter().find(|(open, _)| *open).map(|(_, t)| *t) } /// The topic F1 opens: the hovered button's, else the panel under the /// pointer, else the open dialog's, else none (the manual's first page). pub fn topic_for(app: &App, pointer: Option) -> Option<&'static str> { app.context_help .hovered .or_else(|| { let p = pointer?; app.context_help.zones.iter().find(|(r, _)| r.contains(p)).map(|z| z.1) }) .or_else(|| dialog_topic(app)) } /// Once per frame, before the panels are drawn: handle F1, then forget last /// frame's notes. pub fn frame(app: &mut App, ctx: &egui::Context) { if ctx.input_mut(|i| i.consume_key(egui::Modifiers::NONE, egui::Key::F1)) { let pointer = ctx.input(|i| i.pointer.hover_pos()); let topic = topic_for(app, pointer); app.run("help.open", json!({ "topic": topic.unwrap_or("") })); } app.context_help.hovered = None; app.context_help.zones.clear(); } #[cfg(test)] mod tests { use super::*; use rustfig_engine::Engine; #[test] fn every_mode_button_has_an_existing_topic() { for t in ToolKind::ALL { let f = mode_topic(t).unwrap_or_else(|| panic!("no topic for mode {}", t.id())); assert!(crate::help::find_topic(f).is_some(), "{f}"); } } #[test] fn fixed_topics_exist() { for f in [LIBRARY_TOPIC, INDICATOR_TOPIC, DEPTH_TOPIC, RULER_TOPIC] { assert!(crate::help::find_topic(f).is_some(), "{f}"); } } #[test] fn dialogs_have_topics_and_hover_wins() { let mut app = App::with_engine(Engine::new()); assert_eq!(dialog_topic(&app), None); assert_eq!(topic_for(&app, None), None); app.dialogs.export.open = true; assert_eq!(dialog_topic(&app), Some("export.md")); app.dialogs.export.open = false; for (set, file) in [(0, "preferences.md"), (1, "keys.md"), (2, "mouse.md")] { app.dialogs.global_open = set == 0; app.keys_sheet.open = set == 1; app.mouse_sheet.open = set == 2; let f = dialog_topic(&app).unwrap(); assert_eq!(f, file); assert!(crate::help::find_topic(f).is_some()); } app.mouse_sheet.open = false; app.context_help.zones.push(( egui::Rect::from_min_size(egui::pos2(0.0, 0.0), egui::vec2(10.0, 10.0)), INDICATOR_TOPIC, )); assert_eq!(topic_for(&app, Some(egui::pos2(5.0, 5.0))), Some(INDICATOR_TOPIC)); app.context_help.hovered = Some("chop.md"); assert_eq!(topic_for(&app, Some(egui::pos2(5.0, 5.0))), Some("chop.md")); } } .