Extract the gesture vocabulary from the code that implements it
Every gesture the application has was documented in the comment beside the `TouchArea` that implements it. Excellent comments, and unreachable by anyone not reading the source — which is the FR-UI-4 failure in a different costume: a gesture nobody can find is a feature only its author knows about. Writing them out again in a hand-kept help page is the failure this avoids. Two descriptions of one gesture drift, and it is always the prose that drifts: the code is exercised every time somebody uses the application and the page is exercised never. A help screen confidently describing a double tap the grid stopped honouring last week is worse than no help screen — and the grid did stop honouring one, in the commit before this. So the comment beside the implementation stays the only copy, and a `GESTURE:` block beside it is scanned into two artefacts: `docs/gestures.md` for a reader, and a Rust table for the application to draw a help sheet from. Both committed, both gated, so neither can quietly stop describing the code. It lives in the traceability crate because it is the same operation on the same input — walk the tree, pull structured tags out of comments, render, fail if the committed artefact has moved. Only the vocabulary is new. It scans `ui` and `apps` alone: a gesture needs an interface to be performed on, and excluding `tools` is also what stops the scanner extracting its own worked examples as broken gestures. Fifteen gestures so far, across the library grid and the People screen. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,130 @@
|
||||
// @generated by `cargo run -p traceability -- gestures`. Do not edit.
|
||||
//
|
||||
// TRACES: FR-UI-4
|
||||
//! The gesture vocabulary, as the application states it.
|
||||
//!
|
||||
//! Extracted from the tagged comments beside the code that implements each
|
||||
//! one, so the help sheet cannot describe a gesture the application does not
|
||||
//! have. Edit the comment beside the implementation, then regenerate with
|
||||
//! `cargo run -p traceability -- gestures`.
|
||||
|
||||
/// One documented interaction, as drawn.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct Gesture {
|
||||
pub title: &'static str,
|
||||
/// The place it applies, and the heading it is grouped under.
|
||||
pub section: &'static str,
|
||||
/// Empty where the gesture has no counterpart in that modality.
|
||||
pub touch: &'static str,
|
||||
pub pointer: &'static str,
|
||||
pub keys: &'static str,
|
||||
}
|
||||
|
||||
/// Every gesture, in the order the sections were first seen in the source.
|
||||
pub const GESTURES: &[Gesture] = &[
|
||||
Gesture {
|
||||
title: "Pull a face out of the wrong person",
|
||||
section: "People",
|
||||
touch: "Tap the faces that do not belong, then \"Split off\"",
|
||||
pointer: "Click the faces that do not belong, then \"Split off\"",
|
||||
keys: "",
|
||||
},
|
||||
Gesture {
|
||||
title: "Rule on a suggested face",
|
||||
section: "People",
|
||||
touch: "Tick to confirm it, cross to reject it",
|
||||
pointer: "Tick to confirm it, cross to reject it",
|
||||
keys: "",
|
||||
},
|
||||
Gesture {
|
||||
title: "See a person's photographs",
|
||||
section: "People",
|
||||
touch: "Choose them in the rail, then \"Show photos\"",
|
||||
pointer: "Choose them in the rail, then \"Show photos\"",
|
||||
keys: "",
|
||||
},
|
||||
Gesture {
|
||||
title: "Change how faces are grouped",
|
||||
section: "People",
|
||||
touch: "\"Grouping…\", move the dials, then Regroup",
|
||||
pointer: "\"Grouping…\", move the dials, then Regroup",
|
||||
keys: "",
|
||||
},
|
||||
Gesture {
|
||||
title: "Start selecting several photographs",
|
||||
section: "Library grid",
|
||||
touch: "Press and hold a photograph, or press Select in the header",
|
||||
pointer: "Ctrl-click, or press Select in the header",
|
||||
keys: "",
|
||||
},
|
||||
Gesture {
|
||||
title: "Add or remove one photograph",
|
||||
section: "Library grid",
|
||||
touch: "While selecting, tap it",
|
||||
pointer: "Ctrl-click it",
|
||||
keys: "",
|
||||
},
|
||||
Gesture {
|
||||
title: "Leave selecting",
|
||||
section: "Library grid",
|
||||
touch: "Press Done in the header",
|
||||
pointer: "Press Done in the header",
|
||||
keys: "Escape",
|
||||
},
|
||||
Gesture {
|
||||
title: "Select a range",
|
||||
section: "Library grid",
|
||||
touch: "While selecting, press \"Select to…\", then tap the last photograph of the run",
|
||||
pointer: "Shift-click the last photograph of the run",
|
||||
keys: "",
|
||||
},
|
||||
Gesture {
|
||||
title: "Find photographs with two people in them",
|
||||
section: "Library grid",
|
||||
touch: "Open the People chip on the filter bar, tap each name, then switch the chip beside them to \"all of them\"",
|
||||
pointer: "Open the People chip on the filter bar, click each name, then switch the chip beside them to \"all of them\"",
|
||||
keys: "",
|
||||
},
|
||||
Gesture {
|
||||
title: "Drop the selection but keep selecting",
|
||||
section: "Library grid",
|
||||
touch: "Press Clear in the selection strip",
|
||||
pointer: "Press Clear in the selection strip",
|
||||
keys: "",
|
||||
},
|
||||
Gesture {
|
||||
title: "Select everything the grid is showing",
|
||||
section: "Library grid",
|
||||
touch: "While selecting, press \"Select all\"",
|
||||
pointer: "While selecting, press \"Select all\"",
|
||||
keys: "",
|
||||
},
|
||||
Gesture {
|
||||
title: "Resize the thumbnails",
|
||||
section: "Library grid",
|
||||
touch: "Pinch the grid with two fingers",
|
||||
pointer: "Ctrl and the scroll wheel",
|
||||
keys: "",
|
||||
},
|
||||
Gesture {
|
||||
title: "File photographs in a collection",
|
||||
section: "Library grid",
|
||||
touch: "Drag a photograph — or a whole selection — onto a collection in the sidebar. Starting a drag stops the press becoming a hold, so it cannot leave you in selection mode.",
|
||||
pointer: "Drag a photograph — or a whole selection — onto a collection in the sidebar",
|
||||
keys: "",
|
||||
},
|
||||
Gesture {
|
||||
title: "Open a photograph",
|
||||
section: "Library grid",
|
||||
touch: "Tap it — a single tap, any length",
|
||||
pointer: "Click it",
|
||||
keys: "",
|
||||
},
|
||||
Gesture {
|
||||
title: "Rate a photograph without opening it",
|
||||
section: "Library grid",
|
||||
touch: "Tap a star on the cell",
|
||||
pointer: "Hover the cell, then click a star",
|
||||
keys: "0 to 5 on the selection",
|
||||
},
|
||||
];
|
||||
@@ -127,6 +127,14 @@ component FaceCell inherits Rectangle {
|
||||
}
|
||||
}
|
||||
|
||||
// GESTURE: Pull a face out of the wrong person
|
||||
// where: People
|
||||
// touch: Tap the faces that do not belong, then "Split off"
|
||||
// pointer: Click the faces that do not belong, then "Split off"
|
||||
// why: Grouping over-merges on siblings, on parents and
|
||||
// children, and on the same person a decade apart, so
|
||||
// splitting is as prominent as merging. A tool that can
|
||||
// only merge makes its own errors permanent.
|
||||
TouchArea {
|
||||
clicked => { root.toggle-pick(); }
|
||||
}
|
||||
@@ -139,6 +147,14 @@ component FaceCell inherits Rectangle {
|
||||
// Only a suggestion needs ruling on. Once confirmed, the pair
|
||||
// collapses to the label — there is nothing left to decide, and
|
||||
// leaving the buttons would invite an accidental un-confirm.
|
||||
// GESTURE: Rule on a suggested face
|
||||
// where: People
|
||||
// touch: Tick to confirm it, cross to reject it
|
||||
// pointer: Tick to confirm it, cross to reject it
|
||||
// why: A face is either the system's guess or the user's
|
||||
// judgement, and the two are never conflated. A
|
||||
// rejection is remembered, so the face is not suggested
|
||||
// for that person again.
|
||||
if !face.confirmed: IconButton {
|
||||
icon: "check";
|
||||
clicked => { root.confirm(); }
|
||||
@@ -526,6 +542,13 @@ export component IdentityScreen inherits Rectangle {
|
||||
// The way back from a face to the photographs. This is the
|
||||
// point of having identified anybody, and without it the
|
||||
// screen is a filing cabinet with no drawer handles.
|
||||
// GESTURE: See a person's photographs
|
||||
// where: People
|
||||
// touch: Choose them in the rail, then "Show photos"
|
||||
// pointer: Choose them in the rail, then "Show photos"
|
||||
// why: This is the point of having identified anybody.
|
||||
// Without it the screen is a filing cabinet with no
|
||||
// drawer handles.
|
||||
if root.selected-person >= 0: Button {
|
||||
text: "Show photos";
|
||||
clicked => { root.show-photos(root.selected-person, false); }
|
||||
@@ -556,6 +579,15 @@ export component IdentityScreen inherits Rectangle {
|
||||
// next to the button that applies them and the rail that shows
|
||||
// what they did, and a value changed three screens away from
|
||||
// its effect is a value nobody can tune.
|
||||
// GESTURE: Change how faces are grouped
|
||||
// where: People
|
||||
// touch: "Grouping…", move the dials, then Regroup
|
||||
// pointer: "Grouping…", move the dials, then Regroup
|
||||
// why: The right match confidence is a property of your
|
||||
// library, not of the model. "What would this do?"
|
||||
// answers for this library without writing
|
||||
// anything; names, confirmations and the groups you
|
||||
// have set aside are kept whatever the dials say.
|
||||
Button {
|
||||
text: "Grouping…";
|
||||
active: root.grouping-open;
|
||||
|
||||
Reference in New Issue
Block a user