The "Controls and shortcuts" sheet was drawn by LibraryGrid, so only the grid's Help button and its F1 could open it. Develop, where most of the keys it lists are bound (Ctrl+E, Ctrl+Shift+C, A/D, Z, R, H, [ ]), had no way to it: a photographer who wanted to look a shortcut up had to leave the photograph they wanted it for. The sheet now hangs off the shell beside the export and copy sheets, on a `help-open` property both views set. The grid's Help button and F1 raise it through a callback, and its keys stand down through the `sheet-open` they already honour for the export sheet, so Escape falls through to the shell, which closes it. Develop gains a Help button beside Settings in its top bar, as in the library header, and F1 in its key scope; its keys decline while the sheet is up, as they do for the other two sheets, so nothing behind it is rated or stepped. The book already begins with the Develop section, so from develop it opens where the reader wants it. Tests hold the shape: the sheet is drawn by the shell and not the grid, and develop's opening guard names all three sheets and its F1 opens this one. The new GESTURE: block puts the develop route in the book.
186 lines
7.1 KiB
Rust
186 lines
7.1 KiB
Rust
//! 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,
|
|
/// The manual section that shows the gesture, as a heading anchor; empty
|
|
/// where there is none, and the sheet then offers no "See it".
|
|
pub manual: 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(),
|
|
manual: g.manual.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: "",
|
|
manual: "",
|
|
}
|
|
}
|
|
|
|
#[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);
|
|
}
|
|
|
|
/// The develop view's key handler, from its `KEYMAP:` tag to the end of
|
|
/// the file — enough to hold its opening guard and its bindings.
|
|
fn develop_keys() -> &'static str {
|
|
let app = include_str!("../ui/app.slint");
|
|
let tag = "// KEYMAP: Develop\n key-pressed(event)";
|
|
let at = app
|
|
.find(tag)
|
|
.expect("app.slint no longer has develop's key-pressed handler");
|
|
&app[at..]
|
|
}
|
|
|
|
/// TRACES: FR-UI-4
|
|
/// The sheet is drawn by the shell, so both views can open it. Drawn
|
|
/// inside the grid, it was unreachable from develop — where most of the
|
|
/// keys it lists are bound.
|
|
#[test]
|
|
fn the_help_sheet_is_drawn_by_the_shell_and_not_by_the_grid() {
|
|
let app = include_str!("../ui/app.slint");
|
|
let library = include_str!("../ui/library.slint");
|
|
assert!(app.contains("if root.help-open: GestureSheet {"));
|
|
assert!(!library.contains("GestureSheet {"));
|
|
}
|
|
|
|
/// TRACES: FR-UI-4 | FR-UI-5
|
|
/// F1 opens it from develop, and develop's keys stand down while it is
|
|
/// up, as they do for the export and copy sheets — a key acting behind
|
|
/// the sheet would change a photograph the reader cannot see.
|
|
#[test]
|
|
fn develop_opens_help_on_f1_and_stands_down_behind_it() {
|
|
let keys = develop_keys();
|
|
let guard = keys
|
|
.find("return reject;")
|
|
.expect("develop's handler never declines");
|
|
let opening = &keys[..guard];
|
|
for sheet in ["export-sheet-open", "copy-sheet-open", "help-open"] {
|
|
assert!(
|
|
opening.contains(sheet),
|
|
"develop's keys do not stand down behind `{sheet}`"
|
|
);
|
|
}
|
|
let f1 = keys
|
|
.find("Keys.chord(event) == \"F1\"")
|
|
.expect("develop does not bind F1");
|
|
assert!(
|
|
keys[f1..].lines().nth(1).unwrap_or("").trim() == "root.help-open = true;",
|
|
"develop's F1 does not open the help sheet"
|
|
);
|
|
}
|
|
|
|
#[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
|
|
);
|
|
}
|
|
}
|
|
}
|