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>
181 lines
6.6 KiB
Plaintext
181 lines
6.6 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,
|
|
}
|
|
|
|
// 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();
|
|
|
|
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 {
|
|
text: "How to drive the grid";
|
|
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;
|
|
}
|
|
|
|
if r.heading == "": Text {
|
|
text: r.title;
|
|
color: Theme.ink;
|
|
font-size: Theme.text;
|
|
font-weight: 600;
|
|
wrap: word-wrap;
|
|
}
|
|
|
|
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;
|
|
Button {
|
|
text: "Done";
|
|
primary: true;
|
|
clicked => { root.close(); }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|