//! 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); } #[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 ); } } }