Skip to main content

max / quasi

15.4 KB · 422 lines History Blame Raw
1 //! What the user can reach, and the order they reach it in.
2 //!
3 //! Nothing in a description says this. A webview never had to ask: the browser
4 //! builds the tab order out of the document, and the document is the drawing, so
5 //! the order falls out of the markup a renderer already emitted. A terminal
6 //! draws cells, and a cell knows nothing about the one before it.
7 //!
8 //! So focus order is this renderer's policy, and the policy is: **draw order**.
9 //! A thing is reachable when the description gives it something to call, and it
10 //! comes after whatever was drawn above it. That is the same rule the browser
11 //! applies to a document with no `tabindex` in it, which is the shape every
12 //! screen here has.
13 //!
14 //! The walk below mirrors [`crate::node::draw`] step for step, and it has to:
15 //! the drawing counts reachable things as it passes them and lights the one
16 //! whose number matches, so a walk that visited them in another order would
17 //! light the wrong one. The two are kept together deliberately rather than
18 //! being derived from one traversal, because the drawing needs a rect and this
19 //! needs nothing, and threading a rect through a walk that has no use for one
20 //! was the worse of the two couplings.
21 //!
22 //! # What is reachable
23 //!
24 //! Anything the description gives an address to, plus the two affordances that
25 //! are addresses in everything but name: a row that can be ticked, and a field
26 //! that takes typing. A [`Node::Meter`] and a [`Node::Figure`] are readouts and
27 //! are skipped, and a disabled [`Act`] is drawn and passed over, which is what
28 //! `disabled` means on every host.
29 //!
30 //! # Reach is this module; focus is the view's
31 //!
32 //! Both are this renderer's, and neither is describable. **Reach** is what this
33 //! module computes: which things can take focus, and in what order. **Focus** is
34 //! which reached thing holds the keyboard right now, and it lives in
35 //! [`crate::View`] because it is a fact about where the user has walked rather
36 //! than about the screen. The **focus ring** is what the drawing paints on it.
37 //!
38 //! A description used to be able to claim focus for a control, and
39 //! `makeover-layout` removed the member in 0.19.0 on the grounds that focus is
40 //! fundamentally different per host. The runtime now starts on the first reach
41 //! unconditionally, which is what it did in practice anyway once the user
42 //! pressed anything. The three terms are defined once in `makeover_layout`'s
43 //! crate header, "Reach, focus and the focus ring".
44
45 use makeover_layout as layout;
46 use quasi_router::{Act, Action, Field, Node, Part, Row, Screen, Slot};
47
48 /// One thing the user can reach, and what reaching it offers.
49 #[derive(Debug, Clone, PartialEq, Eq)]
50 pub enum Spot {
51 /// A control. Enter calls it, after its confirmation when it has one.
52 Act {
53 /// What it calls.
54 action: Action,
55 /// What to ask first, if anything.
56 confirm: Option<String>,
57 /// The key that reaches it without walking there.
58 ///
59 /// The one place the description already anticipated a terminal, and
60 /// the runtime is what finally binds it.
61 key: Option<String>,
62 /// The screen's selection this acts on, if it acts on one.
63 ///
64 /// The commit half of a staged tick. The runtime reads the set the view
65 /// is holding and sends it with the call, which is the whole of what
66 /// makes a bulk action work without a line of gathering code.
67 over: Option<String>,
68 },
69 /// Text that goes somewhere. Enter follows it.
70 Link {
71 /// Where it goes.
72 action: Action,
73 },
74 /// A question. Typing edits it; Enter leaves it alone.
75 Field(Box<FieldSpot>),
76 /// The control that answers a whole form.
77 Submit {
78 /// Where the answers go.
79 action: Action,
80 /// The names the form submits, in order, so the runtime can gather the
81 /// values it is holding for them.
82 names: Vec<String>,
83 },
84 /// A row of a list.
85 Row {
86 /// What opening it calls.
87 activate: Option<Action>,
88 /// What ticking it calls, when the tick is itself the write.
89 toggle: Option<Action>,
90 /// Whether it is ticked, and whether it can be.
91 ticked: Option<bool>,
92 /// What its tick contributes to the screen's selection.
93 ///
94 /// `None` on a row that names nothing, which on a screen holding a
95 /// selection is the dead affordance `5f2b8753` was filed for: the box
96 /// is drawn, the key is bound, and the tick has nowhere to go. The
97 /// runtime declines to bind the key in that case rather than binding it
98 /// to nothing.
99 value: Option<String>,
100 /// What it offers without showing: reached by a key here, by
101 /// right-click on a pointer host.
102 menu: Vec<Act>,
103 },
104 /// One option of a selector.
105 Choice {
106 /// What picking it calls.
107 action: Action,
108 },
109 /// The way to the rows a list is not showing.
110 More {
111 /// What asking for more calls.
112 action: Action,
113 },
114 }
115
116 /// A question, and everything the runtime needs to hold what is typed into it.
117 #[derive(Debug, Clone, PartialEq, Eq)]
118 pub struct FieldSpot {
119 /// The name the value is submitted under.
120 pub name: String,
121 /// What kind of value it takes.
122 pub kind: layout::FieldKind,
123 /// What the description offers back, which is what an untouched buffer
124 /// starts from.
125 ///
126 /// Always `None` for a [`layout::FieldKind::Secret`], and that is the whole
127 /// of `39057019`: the description refuses to carry one, on purpose, so the
128 /// runtime's buffer is the only place the typed value has ever lived.
129 pub value: Option<String>,
130 /// The values on offer, for the kinds that offer any.
131 pub options: Vec<String>,
132 /// The longest value it will take, in characters.
133 pub max_length: Option<u32>,
134 /// What changing it calls, for a control that writes on its own.
135 pub changes: Option<Action>,
136 }
137
138 impl Spot {
139 /// What Enter does here, when it does anything.
140 ///
141 /// A field answers `None`: Enter in a text box is not a submit here, the
142 /// way it is in a browser, because a terminal has no implicit submit and
143 /// guessing one would fire a form from the first field the user typed in.
144 #[must_use]
145 pub fn enters(&self) -> Option<&Action> {
146 match self {
147 Self::Act { action, .. }
148 | Self::Link { action }
149 | Self::Submit { action, .. }
150 | Self::Choice { action }
151 | Self::More { action } => Some(action),
152 Self::Row { activate, .. } => activate.as_ref(),
153 Self::Field(_) => None,
154 }
155 }
156
157 /// The question this stands on, when it is one.
158 #[must_use]
159 pub const fn field(&self) -> Option<&FieldSpot> {
160 match self {
161 Self::Field(spot) => Some(spot),
162 _ => None,
163 }
164 }
165 }
166
167 /// A reachable thing, and the region it is in.
168 ///
169 /// The region is here because scrolling needs it. A key that scrolls has to
170 /// scroll something, and the only non-arbitrary answer is the region the user is
171 /// working in, which is the region their focus is in. Carrying it on the walk
172 /// that already visits every reachable thing is cheaper than a second walk that
173 /// would be free to disagree with this one.
174 #[derive(Debug, Clone, PartialEq, Eq)]
175 pub struct Reach {
176 /// The [`Slot::id`] of the region holding it.
177 pub region: String,
178 /// What it is.
179 pub spot: Spot,
180 }
181
182 /// Everything reachable on `screen`, in draw order, with its region.
183 #[must_use]
184 pub fn reaches(screen: &Screen) -> Vec<Reach> {
185 let mut found = Vec::new();
186 for slot in crate::region::reachable(screen) {
187 slot_spots(slot, &mut found);
188 }
189 found
190 }
191
192 /// Everything reachable on `screen`, in draw order.
193 #[must_use]
194 pub fn spots(screen: &Screen) -> Vec<Spot> {
195 reaches(screen)
196 .into_iter()
197 .map(|reach| reach.spot)
198 .collect()
199 }
200
201 /// A region's reachable things.
202 ///
203 /// A region that is still loading has none. It is drawn as the word "Loading"
204 /// and nothing under it is on screen, so anything counted here would be a
205 /// focusable the user cannot see.
206 fn slot_spots(slot: &Slot, found: &mut Vec<Reach>) {
207 if matches!(slot.readiness, layout::Readiness::Pending) {
208 return;
209 }
210 for node in &slot.body {
211 node_spots(node, &slot.id, found);
212 }
213 }
214
215 /// One node's reachable things, in the order it draws them.
216 pub(crate) fn node_spots(node: &Node, region: &str, found: &mut Vec<Reach>) {
217 // Everything below reads better saying what it found rather than how it is
218 // recorded, and the region is the same for every one of them.
219 macro_rules! push {
220 ($spot:expr) => {
221 found.push(Reach {
222 region: region.to_string(),
223 spot: $spot,
224 })
225 };
226 }
227
228 match node {
229 Node::Act(act) => push_act(act, region, found),
230
231 Node::Link { action, .. } => push!(Spot::Link {
232 action: action.clone(),
233 }),
234
235 // A chip carries a route and is drawn as a bracketed label with no
236 // second target in it, which `node.rs` already declined: the `x` a
237 // webview hangs on a chip is a control inside a span. Reaching the chip
238 // is reaching its action, which is the part a terminal can honour.
239 Node::Token(tag) => {
240 if let layout::Token::Chip { .. } = tag.kind
241 && let Some(action) = tag.action.clone()
242 {
243 push!(Spot::Act {
244 action,
245 confirm: None,
246 key: None,
247 over: None,
248 });
249 }
250 }
251
252 Node::StandIn { act, .. } => {
253 if let Some(act) = act {
254 push_act(act, region, found);
255 }
256 }
257
258 Node::Field(field) => push_field(field, region, found),
259
260 Node::Form {
261 action,
262 fields,
263 submit: _,
264 } => {
265 for field in fields {
266 push_field(field, region, found);
267 }
268 push!(Spot::Submit {
269 action: action.clone(),
270 names: fields.iter().map(|field| field.name.clone()).collect(),
271 });
272 }
273
274 Node::List { rows, more } => {
275 for row in rows {
276 push_row(row, region, found);
277 }
278 if let Some(rest) = more {
279 push!(Spot::More {
280 action: rest.action.clone(),
281 });
282 }
283 }
284
285 // Reachable exactly as a list's rows are. A placement changes where a
286 // row is drawn, not whether it can be reached, and the terminal draws
287 // these in the order given -- so tab order and reading order agree
288 // without this having to know anything about the axis.
289 Node::Timeline { entries, .. } => {
290 for entry in entries {
291 push_row(&entry.row, region, found);
292 }
293 }
294
295 // A table's rows are reachable and its cells are not. A cell holding a
296 // control is drawn through `makeover_tui::table`, which lays cells out
297 // by column width and answers no coordinates back, so there is nothing
298 // here that could say where inside a row a control ended up. Reaching
299 // the row is what a terminal can do honestly; reaching the third
300 // control in the fourth cell is a finding.
301 Node::Table { rows, .. } => {
302 for cells in rows {
303 if let Some(activate) = cells.activate.clone() {
304 push!(Spot::Row {
305 activate: Some(activate),
306 toggle: None,
307 ticked: None,
308 value: None,
309 menu: Vec::new(),
310 });
311 }
312 }
313 }
314
315 Node::Select {
316 options, action, ..
317 } => {
318 for (choice, own) in options {
319 // An option carrying nothing falls back to the strip's action
320 // with its value substituted, which is what the description
321 // says the fallback is. An option with neither is a label.
322 let call = own.clone().or_else(|| {
323 action
324 .clone()
325 .map(|action| action.with(Node::SELECTED, choice.value.clone()))
326 });
327 if let Some(action) = call {
328 push!(Spot::Choice { action });
329 }
330 }
331 }
332
333 Node::Region(slot) => slot_spots(slot, found),
334
335 // Readouts and prose. Nothing to call, so nothing to stop on.
336 Node::Heading { .. }
337 | Node::Text { .. }
338 | Node::Rich { .. }
339 | Node::Figure(_)
340 // A picture carries no address of its own -- `src` is where the bytes
341 // are, not somewhere the reader goes -- so there is nothing to stop on.
342 // A picture that is meant to be clicked is one inside a `Link`.
343 | Node::Image(_)
344 | Node::Notice { .. }
345 | Node::Meter(_)
346 | Node::Stats { .. } => {}
347 }
348 }
349
350 /// A control, unless it is disabled.
351 fn push_act(act: &Act, region: &str, found: &mut Vec<Reach>) {
352 if act.state.is_some_and(layout::State::suppresses_interaction) {
353 return;
354 }
355 found.push(Reach {
356 region: region.to_string(),
357 spot: Spot::Act {
358 action: act.action.clone(),
359 confirm: act.confirm.clone(),
360 key: act.key.clone(),
361 over: act.over.clone(),
362 },
363 });
364 }
365
366 /// A question, unless it is hidden.
367 ///
368 /// A hidden field draws nothing and is submitted with the form, so stopping on
369 /// it would be a stop on a blank row.
370 fn push_field(field: &Field, region: &str, found: &mut Vec<Reach>) {
371 if matches!(field.kind, layout::FieldKind::Hidden) {
372 return;
373 }
374 found.push(Reach {
375 region: region.to_string(),
376 spot: Spot::Field(Box::new(FieldSpot {
377 name: field.name.clone(),
378 kind: field.kind,
379 value: field.value.clone(),
380 options: field
381 .options
382 .iter()
383 .map(|choice| choice.value.clone())
384 .collect(),
385 max_length: field.max_length,
386 changes: field.changes.clone(),
387 })),
388 });
389 }
390
391 /// A row: the row itself when the description gives it something to do, then
392 /// whatever its run carries.
393 ///
394 /// Two stops and not one, because they are two things. A row that opens a
395 /// detail pane and also shows a Remove button offers both, and a terminal that
396 /// collapsed them would make the button unreachable or the row unopenable. A
397 /// row that only shows things is passed over entirely, which is the difference
398 /// between a list and a menu.
399 ///
400 /// The row comes first because it is the whole line and the controls sit on it.
401 fn push_row(row: &Row, region: &str, found: &mut Vec<Reach>) {
402 if row.activate.is_some()
403 || row.toggle.is_some()
404 || row.selected.is_some()
405 || !row.menu.is_empty()
406 {
407 found.push(Reach {
408 region: region.to_string(),
409 spot: Spot::Row {
410 activate: row.activate.clone(),
411 toggle: row.toggle.clone(),
412 ticked: row.selected,
413 value: row.value.clone(),
414 menu: row.menu.clone(),
415 },
416 });
417 }
418 for Part { node, .. } in &row.parts {
419 node_spots(node, region, found);
420 }
421 }
422