Put the gesture reference in the application
The document the previous commit generates is for somebody reading the repository. The person who needs it most is holding a tablet, has just discovered that a hold does something, and has nowhere to ask what else does. So the same scan writes a table the application draws: a "Gestures" button beside Settings, a sheet with the same scrim and dismissal as the ones that file and name, and every gesture grouped by where it applies with its touch, pointer and keyboard routes side by side. Not the `why` — that is the argument for the design and belongs in the document; on a phone-sized card it would bury the one line the sheet was opened to read. The sheet's file knows nothing about what a gesture is. It draws the rows it is handed, and the rows come from the generated table, because a help screen with its text typed into it is a second description of one behaviour — and the second description is always the one that goes stale. The commit before this deleted a gesture; a hand-kept sheet would still be describing it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,131 @@
|
||||
//! 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,
|
||||
}
|
||||
|
||||
/// 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(),
|
||||
});
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn g(section: &'static str, title: &'static str) -> Gesture {
|
||||
Gesture {
|
||||
title,
|
||||
section,
|
||||
touch: "Tap",
|
||||
pointer: "",
|
||||
keys: "",
|
||||
}
|
||||
}
|
||||
|
||||
#[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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user