Skip to main content

max / quasi

35.7 KB · 840 lines History Blame Raw
1 //! The state a terminal owns because nothing else will.
2 //!
3 //! This type is the answer to `39057019`, and the finding is worth restating
4 //! because the answer only makes sense next to it. `Field::value` is what a
5 //! handler re-offers after a refused write. It is not what is in the box right
6 //! now, and for a [`layout::FieldKind::Secret`] it is nothing at all, on
7 //! purpose: a password that comes back down the wire is a password in a page
8 //! and in a proxy log. A browser never made anyone notice, because a browser
9 //! owns the contents of an `<input>` and redraws it on every keystroke without
10 //! asking the description for permission.
11 //!
12 //! A terminal owns nothing. So the drawing of an editable screen is not a
13 //! function of the description alone, and the two ways to admit that were:
14 //! hand the renderer a second argument, or have the runtime rewrite the
15 //! description before drawing it.
16 //!
17 //! **The second argument won.** Rewriting keeps [`crate::Tui`] a pure function
18 //! of one argument by making the runtime lie about what the handler said, and
19 //! the lie is not free: `Field::value` refuses to hold a secret, so a runtime
20 //! that wrote the typed password into the description would have had to defeat
21 //! that refusal to draw the dots. The guarantee that no renderer emits a secret
22 //! is worth more than the pure signature, and this way the two facts stay
23 //! separate: the description says what the server offers, and this says what the
24 //! user has done since.
25 //!
26 //! Once it exists it holds the rest of what the browser was quietly providing,
27 //! because it turns out to be the same discovery four times: what is typed,
28 //! what has focus, how far a pane is scrolled, and where the back button goes.
29 //! None of the four is in a description and none of them should be.
30 //!
31 //! An overlay holds a second one of these rather than a fifth field being added
32 //! to this one. `Outcome::Over` draws a whole screen over another, and the
33 //! screen underneath keeps its own reach, focus, edits and scroll while it is
34 //! covered: sharing one `View` between the two would mean dismissing a palette
35 //! took the user's typing and scroll position with it. See `Runtime`'s `under`
36 //! stack, which holds the pair.
37 //!
38 //! One more is of the same kind and is deliberately not held here: where the
39 //! caret sits inside a field. It belongs on this list by nature, and it is
40 //! absent because no described screen needs it yet: the one measured consumer
41 //! is goingson's `search.js`, whose completion list depends on which token the
42 //! caret is inside, and that file stays JS. Saying so here keeps the boundary
43 //! explicit, so the next screen that wants caret-dependent completion knows
44 //! this is where it would land rather than re-asking whether a description
45 //! should carry one. It should not.
46
47 use std::collections::{BTreeMap, BTreeSet};
48 use std::time::Instant;
49
50 use makeover_layout as layout;
51 use quasi_router::{Action, Candidate, Chrome, Frame, Params, Screen};
52
53 use crate::focus::{FieldSpot, Spot};
54 use crate::{Hidden, Local};
55
56 /// The host's side of an outstanding wait.
57 ///
58 /// Two `Option`s rather than an `Option` of a pair, because they arrive at
59 /// different moments: the clock starts when the call goes out and a delivery
60 /// count arrives only if something is watching. Most waits never get one.
61 #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
62 struct Awaited {
63 /// When the call went out.
64 since: Option<Instant>,
65 /// How much has arrived, in the unit the description counted.
66 delivered: Option<u64>,
67 }
68
69 /// What the user has done to a screen since it arrived.
70 ///
71 /// A host makes one beside the screen it is holding and keeps the two together.
72 /// Empty is the honest starting state and it draws exactly what the description
73 /// says, which is what every test that predates this passes.
74 #[derive(Debug, Clone, Default, PartialEq, Eq)]
75 pub struct View {
76 /// What has been typed, by [`Field::name`](quasi_router::Field::name).
77 ///
78 /// Absent means untouched, which is different from present and empty: one
79 /// draws the description's value and the other draws a box the user has
80 /// cleared.
81 edits: BTreeMap<String, String>,
82 /// The value a field's [`writes`](quasi_router::Field::writes) last fired
83 /// with, by field name.
84 ///
85 /// `changes` fires when the value is complete rather than on every
86 /// keystroke, so "complete" needs a baseline: leaving a box nobody altered
87 /// must not write, which is what a browser's `change` already promises and
88 /// what `quasi-webview` therefore already does.
89 ///
90 /// Beside `edits` rather than derived from it, because they answer different
91 /// questions: `edits` is what is in the box and this is what the route has
92 /// been told.
93 written: BTreeMap<String, String>,
94 /// Which reached thing has focus, as an index into [`crate::focus::spots`].
95 ///
96 /// Focus is this renderer's and lives here rather than in a description,
97 /// which is why it survives a redraw: reach is recomputed from the screen,
98 /// focus is a fact about where the user has walked. See
99 /// [`crate::focus`]'s header for the three terms.
100 focus: usize,
101 /// How far each region has been scrolled, in rows, by
102 /// [`Slot::id`](quasi_router::Slot::id).
103 scroll: BTreeMap<String, u16>,
104 /// What has been ticked, by [`Row::value`](quasi_router::Row::value).
105 ///
106 /// The fifth thing the browser was quietly providing, and it arrived last
107 /// because it is the one a browser does *not* fully provide: a checkbox
108 /// owns its own checked state, but nothing gathers the boxes back up, so
109 /// every app wrote that part by hand. Here there is no checkbox to own
110 /// anything, which is what made the hole visible.
111 ///
112 /// A set rather than a map from name to bool. Absent is not ticked, and
113 /// the two spellings of that would otherwise drift.
114 ///
115 /// Only the current screen's set, because a screen names one
116 /// ([`Screen::selection`](quasi_router::Screen::selection)) and a new
117 /// screen is a new set. Which set it is does not need storing: the screen
118 /// beside this one says.
119 ticked: BTreeSet<String>,
120 /// Which branches the reader has folded or unfolded, by
121 /// [`Outline::key`](quasi_router::Outline::key).
122 ///
123 /// And the same discovery as [`shown`](Self::shown) one node kind along:
124 /// the description says a branch is open and that is where the outline
125 /// starts, not where it stays. A browser owns this too and never made
126 /// anyone notice, because folding a row there is an attribute on markup
127 /// the document already holds.
128 ///
129 /// Absent means the description's own answer still stands, so a screen
130 /// arriving with a branch shut draws it shut until the reader says
131 /// otherwise.
132 opened: BTreeMap<String, bool>,
133 /// Which child each region is showing, by
134 /// [`Slot::id`](quasi_router::Slot::id).
135 ///
136 /// And the sixth of the same discovery. A description says a region shows
137 /// one of its children at a time and says which one it started on; where
138 /// the reader has moved to since is this renderer's, exactly as
139 /// [`scroll`](Self::scroll) is. A browser owns this too and never made
140 /// anyone notice, because moving a carousel there is a class on an element
141 /// the document already holds.
142 ///
143 /// Absent means the description's own answer still stands, which is what
144 /// makes an untouched screen draw what the handler said.
145 shown: BTreeMap<String, usize>,
146 /// The action this screen is waiting on, when one is outstanding.
147 ///
148 /// And the seventh of the same discovery: a control that has been pressed
149 /// and has not been answered yet is a fact about this moment, which is why
150 /// it lives here and not in the description. A browser owns it too, and
151 /// htmx expresses it as a class on the element that made the request.
152 ///
153 /// Only an [`Action`] carrying [`Action::awaiting`] ever lands here. The
154 /// rest resolve fast enough that a terminal drawing them busy would be a
155 /// flicker, and the description says which those are.
156 outstanding: Option<Action>,
157 /// When the outstanding call went out, and how much of it has landed.
158 ///
159 /// Wiki `loading-and-progress-standard`, rule 1. `Action::awaiting` carries
160 /// the size of the payload; nothing can describe how much of it has arrived,
161 /// because that is a fact about bytes in flight. So it is the host's to
162 /// report and it lands here beside the action it belongs to.
163 ///
164 /// Both `None` for a host that is not watching, which is the honest common
165 /// case rather than a gap. A bar drawn out of a total alone would be
166 /// claiming somebody is counting.
167 progress: Awaited,
168 /// Which control inside the focused table row the caret has stepped onto,
169 /// as an index into [`Spot::Row`]'s `inside`.
170 ///
171 /// And the eighth of the same discovery. `None` is the ordinary state: the
172 /// caret is on the row itself, which is every stop on every other kind of
173 /// node. A table row is the one stop that has an inside, because
174 /// `makeover_tui::table` answers no coordinates back and its cells
175 /// therefore cannot be stops of their own; see [`crate::focus`]'s header.
176 ///
177 /// Held beside [`focus`](Self::focus) rather than folded into it, so that
178 /// nothing counting reachable things has to know a table is different. Every
179 /// walk in the crate still sees one number per row.
180 inside: Option<usize>,
181 /// The candidates a field's suggestion route answered with, and which one
182 /// the caret is on.
183 ///
184 /// And the ninth of the same discovery this type's header lists: what a
185 /// route has just suggested is a fact about this moment, not about the
186 /// screen the handler described. A browser owns it as a list of elements
187 /// in the document; a terminal owns nothing, so it is here.
188 ///
189 /// One at a time, keyed by field name rather than a map of every field's
190 /// list. A list belongs to the box being typed into, and there is one of
191 /// those: keeping a second field's stale candidates would draw a list under
192 /// a box nobody is in.
193 suggesting: Option<Suggesting>,
194 /// How many slots a repeating question stands in, by
195 /// [`Field::name`](quasi_router::Field::name), where the reader has changed
196 /// it.
197 ///
198 /// And the same discovery as everything else here: the description says
199 /// how many answers it was given, and how many boxes there are *now* is a
200 /// fact about what the reader has done since. A browser owns it as
201 /// elements in the document, so nobody had to name it there; there is no
202 /// document here.
203 ///
204 /// Absent means the description's own count still stands, which is what
205 /// makes an untouched screen draw what the handler said.
206 slots: BTreeMap<String, usize>,
207 }
208
209 /// One field's open suggestion list.
210 ///
211 /// [`View::suggesting`]. The candidates as the route answered them, plus which
212 /// one the caret is on — nothing else, because everything else about the list
213 /// is the description's or the drawing's.
214 #[derive(Debug, Clone, PartialEq, Eq)]
215 pub struct Suggesting {
216 /// The [`Field::name`](quasi_router::Field::name) whose list this is.
217 pub field: String,
218 /// The candidates, in the order the route offered them.
219 pub options: Vec<Candidate>,
220 /// Which one the caret is on, or `None` before an arrow has been pressed.
221 ///
222 /// Nothing highlighted is the state a list arrives in, and it is why Enter
223 /// belongs to the form until an arrow has been pressed: a list that
224 /// highlighted its first entry on arrival would take the Enter that submits
225 /// a form the moment the user paused typing.
226 pub at: Option<usize>,
227 }
228
229 impl View {
230 /// Nothing typed, the first thing focused, nothing scrolled.
231 #[must_use]
232 pub fn new() -> Self {
233 Self::default()
234 }
235
236 /// How many slots this repeating question stands in right now.
237 ///
238 /// `described` is what the description offered, which is
239 /// [`Field::slots`](quasi_router::Field::slots). What the reader has added
240 /// or taken away wins, and the answer is held to the question's own floor
241 /// and ceiling so that no walk can offer a slot the description refuses.
242 #[must_use]
243 pub fn standing(&self, field: &quasi_router::Field) -> usize {
244 let Some(repeat) = &field.repeats else {
245 return 1;
246 };
247 let described = repeat.standing();
248 let count = self.slots.get(&field.name).copied().unwrap_or(described);
249 count
250 .max(repeat.least)
251 .min(repeat.most.unwrap_or(usize::MAX))
252 }
253
254 /// Add a slot to a repeating question, if its ceiling allows another.
255 ///
256 /// No request, which is the third of the three things the member is for:
257 /// the reader creates and destroys slots, and the box that appears is a box
258 /// nothing was asked for.
259 pub fn add_slot(&mut self, field: &quasi_router::Field) {
260 let Some(repeat) = &field.repeats else {
261 return;
262 };
263 let standing = self.standing(field);
264 if !repeat.more(standing) {
265 return;
266 }
267 self.slots.insert(field.name.clone(), standing + 1);
268 }
269
270 /// Take one slot out of a repeating question, if its floor allows one
271 /// fewer.
272 ///
273 /// The answers after it move up, buffer and all, because the names are
274 /// positional: leaving them where they were would submit a hole under the
275 /// name the reader had just emptied and drop the last answer off the end.
276 /// That is the same renumbering `repeat.js` does in the browser.
277 pub fn remove_slot(&mut self, field: &quasi_router::Field, at: usize) {
278 let Some(repeat) = &field.repeats else {
279 return;
280 };
281 let standing = self.standing(field);
282 if at >= standing || !repeat.fewer(standing) {
283 return;
284 }
285 // Every name the slot submits under, which is one for an ordinary
286 // repeating question and one per part for a grouped one. Read off
287 // `Field::instance_fields` rather than spelled here, so this moves the
288 // same names the drawing and the caret walk use.
289 for slot in at..standing - 1 {
290 for (here, next) in field
291 .instance_fields(slot)
292 .iter()
293 .zip(field.instance_fields(slot + 1).iter())
294 {
295 let carried = self
296 .edits
297 .get(&next.name)
298 .cloned()
299 .or_else(|| next.value.clone());
300 match carried {
301 Some(value) => {
302 self.edits.insert(here.name.clone(), value);
303 }
304 None => {
305 self.edits.remove(&here.name);
306 }
307 }
308 }
309 }
310 for last in field.instance_fields(standing - 1) {
311 self.edits.remove(&last.name);
312 }
313 self.slots.insert(field.name.clone(), standing - 1);
314 }
315
316 /// What is in the box: what has been typed, or what the description offers,
317 /// or nothing.
318 ///
319 /// The order is the whole of the type's job. An untouched field shows what
320 /// the handler put there; a touched one shows what the user did, including
321 /// when what they did was empty it.
322 #[must_use]
323 pub fn typed<'a>(&'a self, field: &'a FieldSpot) -> &'a str {
324 self.showing(&field.name, field.value.as_deref())
325 }
326
327 /// The action this view is waiting on.
328 ///
329 /// What the drawing consults to mute a control that has been pressed, and
330 /// what the runtime consults to refuse a second press of it.
331 #[must_use]
332 pub const fn outstanding(&self) -> Option<&Action> {
333 self.outstanding.as_ref()
334 }
335
336 /// Say that this action is outstanding, or that nothing is.
337 ///
338 /// The runtime's to set. A host driving this crate without one is drawing a
339 /// screen it never dispatched from, so there is nothing for it to say here.
340 pub(crate) fn awaiting(&mut self, action: Option<Action>) {
341 self.awaiting_at(action, Instant::now());
342 }
343
344 /// [`awaiting`](Self::awaiting) with the clock handed in, for tests.
345 pub(crate) fn awaiting_at(&mut self, action: Option<Action>, now: Instant) {
346 self.progress = Awaited {
347 since: action.as_ref().map(|_| now),
348 delivered: None,
349 };
350 self.outstanding = action;
351 }
352
353 /// Say how much of the outstanding call has arrived.
354 ///
355 /// The host's to call, as often as it likes, from whatever it is watching:
356 /// bytes off a socket, rows out of an import. In the unit the description
357 /// counted, which is the app's business either way -- see
358 /// `makeover_layout::Awaiting::amount`.
359 ///
360 /// Ignored when nothing is outstanding. A delivery count with no wait
361 /// attached would be drawn against the next call to go out, which is a
362 /// number belonging to the wrong wait.
363 pub fn delivered(&mut self, amount: u64) {
364 if self.outstanding.is_some() {
365 self.progress.delivered = Some(amount);
366 }
367 }
368
369 /// What is known about the wait that is running, at `now`.
370 ///
371 /// Empty when nothing is outstanding, which is what makes the drawing
372 /// degrade to the activity mark rather than to a bar of nothing.
373 #[must_use]
374 pub fn progress_at(&self, now: Instant) -> makeover_tui::piece::Progress {
375 makeover_tui::piece::Progress {
376 delivered: self.progress.delivered,
377 elapsed: self
378 .progress
379 .since
380 .map(|since| now.saturating_duration_since(since)),
381 }
382 }
383
384 /// Whether this is the control that was pressed and has not been answered.
385 #[must_use]
386 pub fn busy(&self, action: &Action) -> bool {
387 self.outstanding.as_ref() == Some(action)
388 }
389
390 /// [`typed`](Self::typed) for a caller holding the described field itself
391 /// rather than a walk's record of it, which is what the drawing has.
392 #[must_use]
393 pub fn showing<'a>(&'a self, name: &str, described: Option<&'a str>) -> &'a str {
394 self.edits
395 .get(name)
396 .map(String::as_str)
397 .or(described)
398 .unwrap_or_default()
399 }
400
401 /// What has been typed into a field by name, if anything has.
402 #[must_use]
403 pub fn edit(&self, name: &str) -> Option<&str> {
404 self.edits.get(name).map(String::as_str)
405 }
406
407 /// The open suggestion list, if it belongs to this field.
408 ///
409 /// Named rather than returned bare so a caller cannot draw one field's
410 /// candidates under another's box.
411 #[must_use]
412 pub fn suggesting(&self, name: &str) -> Option<&Suggesting> {
413 self.suggesting.as_ref().filter(|open| open.field == name)
414 }
415
416 /// The open suggestion list, whichever field owns it.
417 ///
418 /// What the drawing reads once the field that owns it has said where the
419 /// list goes. [`suggesting`](Self::suggesting) is the question a caller
420 /// holding a field asks; this is the one the drawing asks afterwards, and
421 /// keeping them apart is what stops a list being drawn under the wrong box.
422 #[must_use]
423 pub fn suggesting_here(&self) -> Option<&Suggesting> {
424 self.suggesting.as_ref()
425 }
426
427 /// What a field's suggestion route answered with.
428 ///
429 /// Replaces whatever was open, including a list belonging to another field:
430 /// an answer is about the box being typed into, and the previous one is
431 /// about a box the user has left. Highlight starts at nothing, for
432 /// [`Suggesting::at`]'s reason.
433 ///
434 /// An empty answer closes the list rather than opening an empty one. A
435 /// route with nothing to suggest and a route that was never asked leave the
436 /// screen in the same state, which is the honest reading of both.
437 pub(crate) fn suggested(&mut self, field: impl Into<String>, options: Vec<Candidate>) {
438 let field = field.into();
439 self.suggesting = (!options.is_empty()).then_some(Suggesting {
440 field,
441 options,
442 at: None,
443 });
444 }
445
446 /// Move the highlight, wrapping, and skipping nothing.
447 ///
448 /// From nothing, one step forward lands on the first candidate and one back
449 /// on the last, which is the terminal's own idiom and the webview's
450 /// program's.
451 pub(crate) fn highlight(&mut self, by: isize) {
452 let Some(open) = self.suggesting.as_mut() else {
453 return;
454 };
455 let count = open.options.len();
456 if count == 0 {
457 return;
458 }
459 let from = match open.at {
460 Some(at) => at as isize,
461 None if by > 0 => -1,
462 None => 0,
463 };
464 let there = (from + by).rem_euclid(count as isize);
465 open.at = Some(there as usize);
466 }
467
468 /// Take the highlighted candidate, closing the list.
469 ///
470 /// `None` only when nothing is highlighted, which leaves the list open.
471 ///
472 /// The whole candidate rather than its value, because what picking does is
473 /// not always to write: [`Candidate::picks`] carries an action, and only
474 /// the caller can perform one. Absent one the caller writes
475 /// [`Candidate::value`], which is the default and every site that exists
476 /// today.
477 ///
478 /// [`Candidate`] carries no `unavailable`: a suggestion that cannot be
479 /// picked is a row a route should not have offered.
480 pub(crate) fn pick(&mut self) -> Option<Candidate> {
481 let open = self.suggesting.as_ref()?;
482 let candidate = open.options.get(open.at?)?.clone();
483 self.suggesting = None;
484 Some(candidate)
485 }
486
487 /// Put the list away.
488 pub(crate) fn unsuggest(&mut self) {
489 self.suggesting = None;
490 }
491
492 /// Put a value in a box.
493 pub fn set(&mut self, name: impl Into<String>, value: impl Into<String>) {
494 self.edits.insert(name.into(), value.into());
495 }
496
497 /// Whether this value is one the field's `changes` has already fired with.
498 ///
499 /// Falls back to what the description offered, so walking through a box
500 /// without altering it writes nothing.
501 #[must_use]
502 pub fn unwritten(&self, name: &str, value: &str, described: Option<&str>) -> bool {
503 match self.written.get(name) {
504 Some(written) => written != value,
505 None => described.unwrap_or_default() != value,
506 }
507 }
508
509 /// Remember what a field's `changes` fired with.
510 pub fn wrote(&mut self, name: &str, value: &str) {
511 self.written.insert(name.to_owned(), value.to_owned());
512 }
513
514 /// Add a character to a box, starting from whatever is showing in it.
515 pub fn push(&mut self, field: &FieldSpot, ch: char) {
516 let mut value = self.typed(field).to_string();
517 value.push(ch);
518 self.set(&field.name, value);
519 }
520
521 /// Take the last character back out of a box.
522 pub fn backspace(&mut self, field: &FieldSpot) {
523 let mut value = self.typed(field).to_string();
524 value.pop();
525 self.set(&field.name, value);
526 }
527
528 /// Which reachable thing has focus.
529 #[must_use]
530 pub const fn focus(&self) -> usize {
531 self.focus
532 }
533
534 /// Move focus by `steps`, wrapping at both ends.
535 ///
536 /// Wrapping rather than stopping, because a terminal has no scrollbar to
537 /// tell you that you are at the end of the reachable things and pressing tab
538 /// against a dead stop reads as a broken key.
539 pub fn advance(&mut self, steps: isize, reachable: usize) {
540 // Leaving the row leaves what is inside it. Stepping in is a move
541 // within one stop, so moving to another stop cannot carry it along.
542 self.inside = None;
543 if reachable == 0 {
544 self.focus = 0;
545 return;
546 }
547 let count = reachable as isize;
548 let at = self.focus.min(reachable - 1) as isize;
549 self.focus = (at + steps).rem_euclid(count) as usize;
550 }
551
552 /// Focus something in particular, if it is there.
553 pub fn focus_on(&mut self, at: usize, reachable: usize) {
554 if at < reachable {
555 self.focus = at;
556 self.inside = None;
557 }
558 }
559
560 /// Which control inside the focused table row the caret has stepped onto.
561 ///
562 /// `None` on every other stop, and on a row the caret is standing on
563 /// without having stepped in. See [`crate::focus`]'s header for why a table
564 /// row is the one stop with an inside.
565 #[must_use]
566 pub const fn inside(&self) -> Option<usize> {
567 self.inside
568 }
569
570 /// Step into the focused row, or along the controls in it, wrapping at both
571 /// ends.
572 ///
573 /// The first step in lands on the first control going forward and on the
574 /// last going back, which is what stepping into a run means from either
575 /// side. After that it cycles, for [`advance`](Self::advance)'s reason: a
576 /// terminal has nothing to show you that you are at the last control, so a
577 /// key that stops dead reads as a broken key. Leaving is
578 /// [`leave`](Self::leave)'s, on a key of its own.
579 pub fn step_inside(&mut self, steps: isize, controls: usize) {
580 if controls == 0 {
581 self.inside = None;
582 return;
583 }
584 let count = controls as isize;
585 let at = match self.inside {
586 Some(at) => (at.min(controls - 1) as isize + steps).rem_euclid(count),
587 None if steps < 0 => count - 1,
588 None => 0,
589 };
590 self.inside = Some(at as usize);
591 }
592
593 /// Step back out of a row, and say whether the caret was in one.
594 ///
595 /// The answer is what lets Escape mean one thing at a time: it leaves the
596 /// row if the caret is inside one, and otherwise it goes on to mean what it
597 /// meant before.
598 pub fn leave(&mut self) -> bool {
599 self.inside.take().is_some()
600 }
601
602 /// How far a region has been scrolled.
603 #[must_use]
604 pub fn scroll(&self, region: &str) -> u16 {
605 self.scroll.get(region).copied().unwrap_or(0)
606 }
607
608 /// Scroll a region, never above its top.
609 ///
610 /// There is no bottom stop here, and that is deliberate: how far a region
611 /// can scroll is how tall its content is at the width it was given, which
612 /// is a fact the drawing knows and this does not. [`crate::Tui::clamp`] is
613 /// where it gets trimmed, once per draw, with the rect in hand.
614 pub fn scroll_by(&mut self, region: &str, rows: i32) {
615 let at = i32::from(self.scroll(region));
616 let next = u16::try_from((at + rows).max(0)).unwrap_or(u16::MAX);
617 self.scroll.insert(region.to_string(), next);
618 }
619
620 /// Hold a region at this offset.
621 pub fn scrolled_to(&mut self, region: &str, rows: u16) {
622 self.scroll.insert(region.to_string(), rows);
623 }
624
625 /// Which child a region is showing, given what its description says.
626 ///
627 /// [`scroll`](Self::scroll)'s shape with one difference: a scroll has an
628 /// obvious zero and this does not, so the description's own answer is the
629 /// floor rather than the top of the region.
630 #[must_use]
631 pub fn shown(&self, slot: &quasi_router::Slot) -> Option<usize> {
632 match self.shown.get(&slot.id) {
633 Some(at) => Some((*at).min(slot.body.len().saturating_sub(1))),
634 None => slot.current(),
635 }
636 }
637
638 /// Move a region to another of its children, wrapping at both ends.
639 ///
640 /// Wrapping for [`advance`](Self::advance)'s reason: a terminal has nothing
641 /// to show you that you are at the last frame, so a next key that stops
642 /// dead reads as a broken key rather than as the end of the gallery.
643 ///
644 /// A closed dismissible region opens on its first child, which is the only
645 /// reading of "next" that does anything from closed.
646 pub fn show_by(&mut self, slot: &quasi_router::Slot, steps: isize) {
647 let count = slot.body.len();
648 if count == 0 {
649 return;
650 }
651 let at = match self.shown(slot) {
652 Some(at) => (at as isize + steps).rem_euclid(count as isize) as usize,
653 None => 0,
654 };
655 self.shown.insert(slot.id.clone(), at);
656 }
657
658 /// Show a particular child of a region.
659 pub fn show(&mut self, region: &str, at: usize) {
660 self.shown.insert(region.to_string(), at);
661 }
662
663 /// Whether a branch is open, given what its description says.
664 ///
665 /// [`shown`](Self::shown)'s shape, one node kind along, and for the same
666 /// reason: the description says where the outline starts and the reader
667 /// says where it is now. See
668 /// [`Row::open`](quasi_router::Row::open) for why folding a branch asks the
669 /// app nothing.
670 #[must_use]
671 pub fn open(&self, key: &str, described: bool) -> bool {
672 self.opened.get(key).copied().unwrap_or(described)
673 }
674
675 /// Fold an open branch, or open a shut one.
676 pub fn fold(&mut self, key: &str, described: bool) {
677 let open = self.open(key, described);
678 self.opened.insert(key.to_string(), !open);
679 }
680
681 /// Whether this value is ticked.
682 #[must_use]
683 pub fn is_ticked(&self, value: &str) -> bool {
684 self.ticked.contains(value)
685 }
686
687 /// Tick it if it is not, untick it if it is.
688 ///
689 /// Staging, never a write. Wiki `explicit-commit-affordance`: the commit
690 /// control is what locks a change in, and a tick that wrote on its own
691 /// would be the change happening with nothing to mark it.
692 pub fn tick(&mut self, value: &str) {
693 if !self.ticked.remove(value) {
694 self.ticked.insert(value.to_owned());
695 }
696 }
697
698 /// Everything ticked, in order.
699 ///
700 /// Ordered because it is a `BTreeSet`, and that is worth relying on: a
701 /// handler reading [`Params::get_all`] gets the same sequence every run, so
702 /// a test over a bulk action is not sorting the answer first.
703 pub fn ticks(&self) -> impl Iterator<Item = &str> {
704 self.ticked.iter().map(String::as_str)
705 }
706
707 /// Start the described ticks off, for the rows that arrive already ticked.
708 ///
709 /// A description can say a row is ticked, and on a screen that has just
710 /// arrived that claim is the only thing there is. Applied on arrival rather
711 /// than read on every draw, because after that the user's ticks are the
712 /// truth and a redraw that went back to the description would undo them.
713 pub fn seed(&mut self, screen: &Screen) {
714 self.ticked = crate::focus::spots(screen, &Local::none())
715 .iter()
716 .filter_map(|spot| match spot {
717 Spot::Row {
718 ticked: Some(true),
719 value: Some(value),
720 ..
721 } => Some(value.clone()),
722 _ => None,
723 })
724 .collect();
725 }
726
727 /// Forget everything typed and scrolled, and go back to the top.
728 ///
729 /// What a whole new screen means. The boxes on it are different boxes, and
730 /// carrying a buffer across would put what was typed into a password field
731 /// into whatever field happens to share its name on the next screen. A
732 /// selection goes the same way and for the same reason: the rows are
733 /// different rows.
734 pub fn reset(&mut self) {
735 self.edits.clear();
736 self.scroll.clear();
737 self.ticked.clear();
738 self.shown.clear();
739 self.focus = 0;
740 self.inside = None;
741 // A screen that has arrived is the answer to whatever was outstanding,
742 // or is a different place entirely. Either way nothing on it has been
743 // pressed yet.
744 self.outstanding = None;
745 self.progress = Awaited::default();
746 }
747
748 /// The values a form submits, gathered for `names` in the order given.
749 ///
750 /// A checkbox is here by presence, the way HTML submits one, so a box that
751 /// is not ticked sends nothing rather than sending an empty string. That is
752 /// [`Field::value`](quasi_router::Field::value)'s own convention read back
753 /// out.
754 #[must_use]
755 pub fn submission(&self, names: &[String], spots: &[Spot]) -> Params {
756 let mut params = Params::new();
757 for name in names {
758 let Some(field) = spots
759 .iter()
760 .filter_map(Spot::field)
761 .find(|field| &field.name == name)
762 else {
763 continue;
764 };
765 let value = self.typed(field);
766 if matches!(field.kind, layout::FieldKind::Checkbox)
767 && value != quasi_router::Node::SELECTED
768 {
769 continue;
770 }
771 params.insert(name.clone(), value.to_string());
772 }
773 params
774 }
775
776 /// Drop anything held for a field the screen no longer has.
777 ///
778 /// A fragment can replace a region holding half a form, and the buffers for
779 /// the fields that went away would otherwise ride along and be submitted by
780 /// the next form that happens to name one of them.
781 /// The frame is passed because the caret may legitimately be standing on
782 /// one of its verbs, which is a place past the end of the screen's own
783 /// spots. Clamping to the screen alone would take the caret off Send every
784 /// time a fragment landed anywhere on the page. The chrome is passed for
785 /// the same reason one lifetime along: a panel's controls sit past the
786 /// frame's verbs, and a field inside one is not the screen's to drop.
787 pub fn prune(&mut self, screen: &Screen, frame: &Frame, chrome: &Chrome) {
788 // Every described spot, including the ones inside a region that does
789 // not apply right now: a section the reader has toggled shut still
790 // holds what they typed into it, and dropping those buffers would lose
791 // the draft on the way back. `079a011e`.
792 let spots: Vec<Spot> =
793 crate::focus::reaches_chromed(screen, frame, chrome, &Local::of(&Hidden::none(), self))
794 .into_iter()
795 .map(|reach| reach.spot)
796 .collect();
797 // The caret, though, is an index into what is on the screen.
798 let hidden = crate::reveal::hidden(screen, chrome, self);
799 let stops =
800 crate::focus::reaches_chromed(screen, frame, chrome, &Local::of(&hidden, self)).len();
801 let live: Vec<&str> = spots
802 .iter()
803 .filter_map(Spot::field)
804 .map(|field| field.name.as_str())
805 .collect();
806 self.edits.retain(|name, _| live.contains(&name.as_str()));
807 // The questions still on the screen that repeat, read back off the
808 // names their slots submit under. `60d1753c`: a count held for a
809 // question a fragment took away would decide how many boxes a later
810 // screen's question of the same name stands in.
811 //
812 // After the buffers, and reading the same walk: the walk was made with
813 // the counts still in place, so a slot the reader added is a live name
814 // and its buffer survives.
815 let repeating: Vec<&str> = spots
816 .iter()
817 .filter_map(|spot| match spot {
818 // The question's own controls name it whatever the reader has
819 // done, including having removed every slot: a count of zero is
820 // the state a walk over the boxes alone cannot see, and
821 // dropping it here would bring the slots back on the next
822 // fragment.
823 Spot::Repeat { field, .. } => Some(field.name.as_str()),
824 _ => None,
825 })
826 .collect();
827 self.slots
828 .retain(|name, _| repeating.contains(&name.as_str()));
829 let was = self.focus;
830 self.focus = self.focus.min(stops.saturating_sub(1));
831 // A fragment that moved the caret moved it to another stop, and the
832 // control it had stepped into belonged to the row it left. Clamping the
833 // index against the new row instead would keep the ring inside a row the
834 // user is no longer on.
835 if self.focus != was {
836 self.inside = None;
837 }
838 }
839 }
840