Skip to main content

max / quasi

14.7 KB · 408 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 // A table's rows are reachable and its cells are not. A cell holding a
286 // control is drawn through `makeover_tui::table`, which lays cells out
287 // by column width and answers no coordinates back, so there is nothing
288 // here that could say where inside a row a control ended up. Reaching
289 // the row is what a terminal can do honestly; reaching the third
290 // control in the fourth cell is a finding.
291 Node::Table { rows, .. } => {
292 for cells in rows {
293 if let Some(activate) = cells.activate.clone() {
294 push!(Spot::Row {
295 activate: Some(activate),
296 toggle: None,
297 ticked: None,
298 value: None,
299 menu: Vec::new(),
300 });
301 }
302 }
303 }
304
305 Node::Select {
306 options, action, ..
307 } => {
308 for (choice, own) in options {
309 // An option carrying nothing falls back to the strip's action
310 // with its value substituted, which is what the description
311 // says the fallback is. An option with neither is a label.
312 let call = own.clone().or_else(|| {
313 action
314 .clone()
315 .map(|action| action.with(Node::SELECTED, choice.value.clone()))
316 });
317 if let Some(action) = call {
318 push!(Spot::Choice { action });
319 }
320 }
321 }
322
323 Node::Region(slot) => slot_spots(slot, found),
324
325 // Readouts and prose. Nothing to call, so nothing to stop on.
326 Node::Heading { .. }
327 | Node::Text { .. }
328 | Node::Rich { .. }
329 | Node::Figure(_)
330 | Node::Notice { .. }
331 | Node::Meter(_)
332 | Node::Stats { .. } => {}
333 }
334 }
335
336 /// A control, unless it is disabled.
337 fn push_act(act: &Act, region: &str, found: &mut Vec<Reach>) {
338 if act.state.is_some_and(layout::State::suppresses_interaction) {
339 return;
340 }
341 found.push(Reach {
342 region: region.to_string(),
343 spot: Spot::Act {
344 action: act.action.clone(),
345 confirm: act.confirm.clone(),
346 key: act.key.clone(),
347 over: act.over.clone(),
348 },
349 });
350 }
351
352 /// A question, unless it is hidden.
353 ///
354 /// A hidden field draws nothing and is submitted with the form, so stopping on
355 /// it would be a stop on a blank row.
356 fn push_field(field: &Field, region: &str, found: &mut Vec<Reach>) {
357 if matches!(field.kind, layout::FieldKind::Hidden) {
358 return;
359 }
360 found.push(Reach {
361 region: region.to_string(),
362 spot: Spot::Field(Box::new(FieldSpot {
363 name: field.name.clone(),
364 kind: field.kind,
365 value: field.value.clone(),
366 options: field
367 .options
368 .iter()
369 .map(|choice| choice.value.clone())
370 .collect(),
371 max_length: field.max_length,
372 changes: field.changes.clone(),
373 })),
374 });
375 }
376
377 /// A row: the row itself when the description gives it something to do, then
378 /// whatever its run carries.
379 ///
380 /// Two stops and not one, because they are two things. A row that opens a
381 /// detail pane and also shows a Remove button offers both, and a terminal that
382 /// collapsed them would make the button unreachable or the row unopenable. A
383 /// row that only shows things is passed over entirely, which is the difference
384 /// between a list and a menu.
385 ///
386 /// The row comes first because it is the whole line and the controls sit on it.
387 fn push_row(row: &Row, region: &str, found: &mut Vec<Reach>) {
388 if row.activate.is_some()
389 || row.toggle.is_some()
390 || row.selected.is_some()
391 || !row.menu.is_empty()
392 {
393 found.push(Reach {
394 region: region.to_string(),
395 spot: Spot::Row {
396 activate: row.activate.clone(),
397 toggle: row.toggle.clone(),
398 ticked: row.selected,
399 value: row.value.clone(),
400 menu: row.menu.clone(),
401 },
402 });
403 }
404 for Part { node, .. } in &row.parts {
405 node_spots(node, region, found);
406 }
407 }
408