The help sheet says which move does a thing, and the manual has a picture of the thing being done, but nothing joined the two: a user reading "Pinch it with two fingers" had no way from there to the GIF of it. A GESTURE tag takes an optional `manual:` field naming a heading of docs/manual/README.md by its anchor. The scan checks every one against the anchors the bundled page is rendered with and fails when the manual has no such heading, so renaming a section cannot leave the sheet linking to the top of the page; gestures-check carries the same failure into CI. The anchor goes into gesture_book.rs as a new field, and into docs/gestures.md as a "See it" link to manual/README.md#anchor. The help sheet draws a "See it" button beside the title of each gesture that has one, which opens the bundled manual at that section. The field is additive: a tag without it is unchanged, and no gesture carries one yet.
137 lines
5.1 KiB
Rust
137 lines
5.1 KiB
Rust
//! 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<Row> {
|
|
rows_from(GESTURES)
|
|
}
|
|
|
|
fn rows_from(gestures: &[Gesture]) -> Vec<Row> {
|
|
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
|
|
);
|
|
}
|
|
}
|
|
}
|