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.
214 lines
8.4 KiB
Plaintext
214 lines
8.4 KiB
Plaintext
// TRACES: FR-UI-2 | FR-UI-4
|
|
//
|
|
// The gesture reference, as a sheet.
|
|
//
|
|
// # Why the application carries one at all
|
|
//
|
|
// FR-UI-4: a gesture with no visible counterpart is a feature only its author
|
|
// knows about. Most of the grid's vocabulary now has one — Select, "Select to…"
|
|
// and Select all are buttons that say what they do — but that a *hold* does the
|
|
// same thing faster, or that two fingers resize the thumbnails, could still only
|
|
// be discovered by accident. The buttons make the gestures usable; this makes
|
|
// them knowable.
|
|
//
|
|
// # Why the rows come from Rust and not from this file
|
|
//
|
|
// Every line here is generated from the comment beside the code that implements
|
|
// the gesture (`gesture_book.rs`, and `tools/traceability` that writes it). A
|
|
// sheet with the text typed into it would be a second description of one
|
|
// behaviour, and the second description is always the one that goes stale: the
|
|
// code is exercised whenever somebody uses the application, and the help screen
|
|
// is exercised never. This file draws whatever it is handed and knows nothing
|
|
// about what a gesture is.
|
|
|
|
import { Theme } from "theme.slint";
|
|
import { Button, Caption } from "widgets.slint";
|
|
|
|
// One line of the sheet: a section heading, or a gesture and its routes.
|
|
//
|
|
// Flat, and not a list of lists, because a sheet whose sections scrolled
|
|
// independently is not one list — it is several, in a box. `heading` non-empty
|
|
// is what marks the two apart; Slint has no sum type to say it better.
|
|
export struct GestureRow {
|
|
heading: string,
|
|
title: string,
|
|
// Empty where the gesture has no counterpart in that modality. Drawn as
|
|
// nothing at all rather than as an empty label, so a touch-only gesture
|
|
// does not read as one whose pointer half is broken.
|
|
touch: string,
|
|
pointer: string,
|
|
keys: string,
|
|
// The manual section that shows the gesture being made, as a heading
|
|
// anchor; empty where the manual has none. Checked against the manual by
|
|
// the generator, so a non-empty one always lands on a heading.
|
|
manual: string,
|
|
}
|
|
|
|
// One route: how a modality performs the gesture.
|
|
component Route inherits HorizontalLayout {
|
|
in property <string> modality;
|
|
in property <string> how;
|
|
|
|
spacing: Theme.gap-sm;
|
|
|
|
Caption {
|
|
text: root.modality;
|
|
// A fixed column so the three routes line up down the sheet. Without
|
|
// it "Touch", "Pointer" and "Keyboard" each set their own left edge for
|
|
// the text beside them, and a list of forty reads as ragged prose.
|
|
width: 64px;
|
|
horizontal-alignment: right;
|
|
}
|
|
|
|
Text {
|
|
text: root.how;
|
|
color: Theme.ink;
|
|
font-size: Theme.text-sm;
|
|
wrap: word-wrap;
|
|
horizontal-stretch: 1;
|
|
}
|
|
}
|
|
|
|
export component GestureSheet inherits Rectangle {
|
|
in property <[GestureRow]> rows;
|
|
|
|
callback close();
|
|
/// Open the manual, at a section's anchor or, empty, at the top.
|
|
callback open-manual(string);
|
|
|
|
background: #000000CC;
|
|
|
|
// Swallows the taps that miss the card, and closes. First, so the card's
|
|
// own controls sit above it — the same scrim, card and dismissal the
|
|
// library's other sheets use, because a user who has filed a selection
|
|
// already knows how this works.
|
|
TouchArea {
|
|
clicked => { root.close(); }
|
|
}
|
|
|
|
Rectangle {
|
|
width: min(520px, parent.width - 2 * Theme.gap-lg);
|
|
height: min(560px, parent.height - 2 * Theme.gap-lg);
|
|
x: (parent.width - self.width) / 2;
|
|
// Centred, unlike the naming sheet: nothing here takes the keyboard, so
|
|
// there is no keyboard to sit above.
|
|
y: (parent.height - self.height) / 2;
|
|
background: Theme.surface;
|
|
border-radius: Theme.radius;
|
|
border-width: 1px;
|
|
border-color: Theme.rule;
|
|
|
|
// Stops a press on the card reaching the scrim behind it.
|
|
TouchArea { }
|
|
|
|
VerticalLayout {
|
|
padding: Theme.gap-lg;
|
|
spacing: Theme.gap;
|
|
|
|
Text {
|
|
// The book covers every place, and opens on Develop — a
|
|
// title naming the grid was wrong about the first thing
|
|
// under it.
|
|
text: "Controls and shortcuts";
|
|
color: Theme.ink;
|
|
font-size: Theme.text-lg;
|
|
font-weight: 600;
|
|
}
|
|
|
|
// The list scrolls; the title and the button do not, so a long
|
|
// vocabulary never pushes the way out off the bottom of the card.
|
|
Flickable {
|
|
vertical-stretch: 1;
|
|
viewport-width: self.width;
|
|
viewport-height: list.preferred-height;
|
|
|
|
list := VerticalLayout {
|
|
width: parent.viewport-width;
|
|
spacing: Theme.gap-sm;
|
|
alignment: start;
|
|
|
|
for r[i] in root.rows: VerticalLayout {
|
|
spacing: 2px;
|
|
|
|
// A heading and a gesture are the same row type, so
|
|
// each half is drawn under its own condition rather
|
|
// than by two repeaters over one filtered list — which
|
|
// would be two passes that could disagree about order.
|
|
// Space above a heading, and none above the first:
|
|
// the gap is what separates a section from the one
|
|
// before it, and there is nothing before the first.
|
|
//
|
|
// An empty Rectangle rather than a `y` offset on the
|
|
// heading — a child of a layout may not set its own
|
|
// `y`, because the layout is already setting it.
|
|
if r.heading != "" && i > 0: Rectangle {
|
|
height: Theme.gap;
|
|
}
|
|
|
|
if r.heading != "": Text {
|
|
text: r.heading;
|
|
color: Theme.ink-dim;
|
|
font-size: Theme.text-sm;
|
|
font-weight: 700;
|
|
}
|
|
|
|
// The title, and beside it "See it" where the manual
|
|
// shows the gesture. On the title's line rather than
|
|
// under the routes, so it is found while reading the
|
|
// title — the moment of "what does that look like?".
|
|
if r.heading == "": HorizontalLayout {
|
|
spacing: Theme.gap;
|
|
|
|
Text {
|
|
text: r.title;
|
|
color: Theme.ink;
|
|
font-size: Theme.text;
|
|
font-weight: 600;
|
|
wrap: word-wrap;
|
|
horizontal-stretch: 1;
|
|
vertical-alignment: center;
|
|
}
|
|
|
|
if r.manual != "": Button {
|
|
text: "See it";
|
|
accessible-label: "See " + r.title + " in the manual";
|
|
clicked => { root.open-manual(r.manual); }
|
|
}
|
|
}
|
|
|
|
if r.touch != "": Route {
|
|
modality: "Touch";
|
|
how: r.touch;
|
|
}
|
|
if r.pointer != "": Route {
|
|
modality: "Pointer";
|
|
how: r.pointer;
|
|
}
|
|
if r.keys != "": Route {
|
|
modality: "Keyboard";
|
|
how: r.keys;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
HorizontalLayout {
|
|
alignment: end;
|
|
spacing: Theme.gap;
|
|
// The manual is the other half of this sheet: the sheet says
|
|
// which move does a thing, the manual shows the thing being
|
|
// done. From the one place a puzzled user already is.
|
|
Button {
|
|
text: "Manual";
|
|
clicked => { root.open-manual(""); }
|
|
}
|
|
Button {
|
|
text: "Done";
|
|
primary: true;
|
|
clicked => { root.close(); }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|