Files
DarkRoom/ui/dr-ui/ui/gestures.slint
T
dtourolle cc905fa2ed Put a scrollbar on the controls and shortcuts sheet
The gesture book runs to several screens inside a card that otherwise
looks complete. Same wrapping as the develop column. The bar is drawn
over the list's right edge, so it overlaps the last few pixels of each
"See it" button.
2026-09-26 07:25:45 -04:00

228 lines
9.2 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, ScrollBar } 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.
Rectangle {
vertical-stretch: 1;
book-flick := Flickable {
width: parent.width;
height: parent.height;
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;
}
}
}
}
// A bar for a pointer: the book runs to several screens, and the bar is
// what says so on a card that otherwise looks complete.
ScrollBar {
x: parent.width - self.width;
y: 0px;
viewport-y <=> book-flick.viewport-y;
viewport-height: book-flick.viewport-height;
visible-height: book-flick.height;
}
}
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(); }
}
}
}
}
}