//! TRACES: FR-UI-2 | FR-UI-4 //! The gesture help sheet's model. //! //! FR-UI-4's rule is that a gesture with no visible counterpart is a feature //! only its author knows about. Most of the grid's vocabulary now *has* a //! visible counterpart — Select, "Select to…", Select all are buttons — but //! knowing that a hold does the same thing faster, or that two fingers resize //! the thumbnails, still had to be discovered by accident. //! //! This is the list that says so. It is built from [`crate::gesture_book`], //! which is generated from the comment beside each implementation, so a sheet //! describing a gesture the application does not have is not possible to write: //! there is no file to write it in. //! //! # Why the rows are flat //! //! A section heading and a gesture are one model here, distinguished by //! `heading` being non-empty, rather than a list of lists. Slint has no nested //! repeater that keeps its scrolling in one place, and a sheet whose sections //! scrolled independently is not one list — it is several, in a box. use crate::gesture_book::{Gesture, GESTURES}; /// One line of the sheet: a heading, or a gesture with its routes. #[derive(Debug, Clone, PartialEq, Eq, Default)] pub struct Row { /// Non-empty on a section heading, and then nothing else is set. /// /// The sheet decides which of the two a row is by asking whether this is /// empty, in Slint, where the drawing happens. There is deliberately no /// `is_heading()` beside it: a predicate here that the drawing code did not /// call would be a second definition of the same distinction, free to drift /// from the one that is actually used. pub heading: String, pub title: String, /// Empty where the gesture has no counterpart in that modality; the sheet /// draws nothing rather than an empty label. pub touch: String, pub pointer: String, pub keys: String, /// The manual section that shows the gesture, as a heading anchor; empty /// where there is none, and the sheet then offers no "See it". pub manual: String, } /// Build the sheet's rows from the generated table. /// /// The table is already ordered so a section's gestures are together — the /// generator groups it that way — so this emits a heading whenever the section /// changes rather than sorting again. Two passes that both decide the order is /// how the document and the sheet come to disagree. pub fn rows() -> Vec { rows_from(GESTURES) } fn rows_from(gestures: &[Gesture]) -> Vec { let mut out = Vec::with_capacity(gestures.len() + 4); let mut section = ""; for g in gestures { if g.section != section { section = g.section; out.push(Row { heading: g.section.to_string(), ..Row::default() }); } out.push(Row { heading: String::new(), title: g.title.to_string(), touch: g.touch.to_string(), pointer: g.pointer.to_string(), keys: g.keys.to_string(), manual: g.manual.to_string(), }); } out } #[cfg(test)] mod tests { use super::*; fn g(section: &'static str, title: &'static str) -> Gesture { Gesture { title, section, touch: "Tap", pointer: "", keys: "", manual: "", } } #[test] fn each_section_gets_one_heading_before_its_gestures() { let rows = rows_from(&[g("Grid", "A"), g("Grid", "B"), g("People", "C")]); let shape: Vec<&str> = rows .iter() .map(|r| if r.heading.is_empty() { "g" } else { "H" }) .collect(); assert_eq!(shape, ["H", "g", "g", "H", "g"]); assert_eq!(rows[0].heading, "Grid"); assert_eq!(rows[3].heading, "People"); } /// The generated table groups sections together, so a repeat is a bug in /// the generator rather than something to defend against by sorting here — /// but a heading emitted twice would silently split a section in the sheet /// while the document showed it whole, so it is worth stating. #[test] fn a_heading_is_not_repeated_within_a_run() { let rows = rows_from(&[g("Grid", "A"), g("Grid", "B"), g("Grid", "C")]); assert_eq!(rows.iter().filter(|r| !r.heading.is_empty()).count(), 1); } /// The develop view's key handler, from its `KEYMAP:` tag to the end of /// the file — enough to hold its opening guard and its bindings. fn develop_keys() -> &'static str { let app = include_str!("../ui/app.slint"); let tag = "// KEYMAP: Develop\n key-pressed(event)"; let at = app .find(tag) .expect("app.slint no longer has develop's key-pressed handler"); &app[at..] } /// TRACES: FR-UI-4 /// The sheet is drawn by the shell, so both views can open it. Drawn /// inside the grid, it was unreachable from develop — where most of the /// keys it lists are bound. #[test] fn the_help_sheet_is_drawn_by_the_shell_and_not_by_the_grid() { let app = include_str!("../ui/app.slint"); let library = include_str!("../ui/library.slint"); assert!(app.contains("if root.help-open: GestureSheet {")); assert!(!library.contains("GestureSheet {")); } /// TRACES: FR-UI-4 | FR-UI-5 /// F1 opens it from develop, and develop's keys stand down while it is /// up, as they do for the export and copy sheets — a key acting behind /// the sheet would change a photograph the reader cannot see. #[test] fn develop_opens_help_on_f1_and_stands_down_behind_it() { let keys = develop_keys(); let guard = keys .find("return reject;") .expect("develop's handler never declines"); let opening = &keys[..guard]; for sheet in ["export-sheet-open", "copy-sheet-open", "help-open"] { assert!( opening.contains(sheet), "develop's keys do not stand down behind `{sheet}`" ); } let f1 = keys .find("Keys.chord(event) == \"F1\"") .expect("develop does not bind F1"); assert!( keys[f1..].lines().nth(1).unwrap_or("").trim() == "root.help-open = true;", "develop's F1 does not open the help sheet" ); } #[test] fn nothing_in_makes_nothing_out() { assert!(rows_from(&[]).is_empty()); } /// The sheet is only worth opening if there is something in it, and the /// generated table is the only thing that can put something there. #[test] fn the_real_table_is_not_empty_and_every_row_can_be_performed() { let rows = rows(); assert!(rows.len() > 5, "the generated gesture table looks empty"); for r in rows.iter().filter(|r| r.heading.is_empty()) { assert!(!r.title.is_empty()); assert!( !r.touch.is_empty() || !r.pointer.is_empty() || !r.keys.is_empty(), "`{}` tells the user no way to perform it", r.title ); } } }