max / quasi
| 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 ; |
| 48 | use Instant; |
| 49 | |
| 50 | use makeover_layout as layout; |
| 51 | use ; |
| 52 | |
| 53 | use crate; |
| 54 | use crate::; |
| 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 | |
| 62 | |
| 63 | /// When the call went out. |
| 64 | since: , |
| 65 | /// How much has arrived, in the unit the description counted. |
| 66 | delivered: , |
| 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 | |
| 75 | |
| 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: , |
| 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: , |
| 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: , |
| 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: , |
| 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: , |
| 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: , |
| 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: , |
| 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: , |
| 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: , |
| 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: , |
| 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 | |
| 215 | |
| 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: , |
| 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: , |
| 227 | |
| 228 | |
| 229 | |
| 230 | /// Nothing typed, the first thing focused, nothing scrolled. |
| 231 | |
| 232 | |
| 233 | Selfdefault |
| 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 | |
| 243 | |
| 244 | let Some = &field.repeats else |
| 245 | return 1; |
| 246 | ; |
| 247 | let described = repeat.standing; |
| 248 | let count = self.slots.get.copied.unwrap_or; |
| 249 | count |
| 250 | .max |
| 251 | .min |
| 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 | |
| 260 | let Some = &field.repeats else |
| 261 | return; |
| 262 | ; |
| 263 | let standing = self.standing; |
| 264 | if !repeat.more |
| 265 | return; |
| 266 | |
| 267 | self.slots.insert; |
| 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 | |
| 278 | let Some = &field.repeats else |
| 279 | return; |
| 280 | ; |
| 281 | let standing = self.standing; |
| 282 | if at >= standing || !repeat.fewer |
| 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 in field |
| 291 | .instance_fields |
| 292 | .iter |
| 293 | .zip |
| 294 | |
| 295 | let carried = self |
| 296 | .edits |
| 297 | .get |
| 298 | .cloned |
| 299 | .or_else; |
| 300 | match carried |
| 301 | Some => |
| 302 | self.edits.insert; |
| 303 | |
| 304 | None => |
| 305 | self.edits.remove; |
| 306 | |
| 307 | |
| 308 | |
| 309 | |
| 310 | for last in field.instance_fields |
| 311 | self.edits.remove; |
| 312 | |
| 313 | self.slots.insert; |
| 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 | |
| 323 | |
| 324 | self.showing |
| 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 | |
| 332 | pub const |
| 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 |
| 341 | self.awaiting_at; |
| 342 | |
| 343 | |
| 344 | /// [`awaiting`](Self::awaiting) with the clock handed in, for tests. |
| 345 | pub |
| 346 | self.progress = Awaited |
| 347 | since: action.as_ref.map, |
| 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 | |
| 364 | if self.outstanding.is_some |
| 365 | self.progress.delivered = Some; |
| 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 | |
| 374 | |
| 375 | Progress |
| 376 | delivered: self.progress.delivered, |
| 377 | elapsed: self |
| 378 | .progress |
| 379 | .since |
| 380 | .map, |
| 381 | |
| 382 | |
| 383 | |
| 384 | /// Whether this is the control that was pressed and has not been answered. |
| 385 | |
| 386 | |
| 387 | self.outstanding.as_ref == Some |
| 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 | |
| 393 | |
| 394 | self.edits |
| 395 | .get |
| 396 | .map |
| 397 | .or |
| 398 | .unwrap_or_default |
| 399 | |
| 400 | |
| 401 | /// What has been typed into a field by name, if anything has. |
| 402 | |
| 403 | |
| 404 | self.edits.get.map |
| 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 | |
| 412 | |
| 413 | self.suggesting.as_ref.filter |
| 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 | |
| 423 | |
| 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 |
| 438 | let field = field.into; |
| 439 | self.suggesting = .then_some |
| 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 |
| 452 | let Some = 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 as isize, |
| 461 | None if by > 0 => -1, |
| 462 | None => 0, |
| 463 | ; |
| 464 | let there = .rem_euclid; |
| 465 | open.at = Some; |
| 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 |
| 481 | let open = self.suggesting.as_ref?; |
| 482 | let candidate = open.options.get?.clone; |
| 483 | self.suggesting = None; |
| 484 | Some |
| 485 | |
| 486 | |
| 487 | /// Put the list away. |
| 488 | pub |
| 489 | self.suggesting = None; |
| 490 | |
| 491 | |
| 492 | /// Put a value in a box. |
| 493 | |
| 494 | self.edits.insert; |
| 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 | |
| 502 | |
| 503 | match self.written.get |
| 504 | Some => written != value, |
| 505 | None => described.unwrap_or_default != value, |
| 506 | |
| 507 | |
| 508 | |
| 509 | /// Remember what a field's `changes` fired with. |
| 510 | |
| 511 | self.written.insert; |
| 512 | |
| 513 | |
| 514 | /// Add a character to a box, starting from whatever is showing in it. |
| 515 | |
| 516 | let mut value = self.typed.to_string; |
| 517 | value.push; |
| 518 | self.set; |
| 519 | |
| 520 | |
| 521 | /// Take the last character back out of a box. |
| 522 | |
| 523 | let mut value = self.typed.to_string; |
| 524 | value.pop; |
| 525 | self.set; |
| 526 | |
| 527 | |
| 528 | /// Which reachable thing has focus. |
| 529 | |
| 530 | pub const |
| 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 | |
| 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 as isize; |
| 549 | self.focus = .rem_euclid as usize; |
| 550 | |
| 551 | |
| 552 | /// Focus something in particular, if it is there. |
| 553 | |
| 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 | |
| 566 | pub const |
| 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 | |
| 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 => .rem_euclid, |
| 587 | None if steps < 0 => count - 1, |
| 588 | None => 0, |
| 589 | ; |
| 590 | self.inside = Some; |
| 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 | |
| 599 | self.inside.take.is_some |
| 600 | |
| 601 | |
| 602 | /// How far a region has been scrolled. |
| 603 | |
| 604 | |
| 605 | self.scroll.get.copied.unwrap_or |
| 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 | |
| 615 | let at = i32from; |
| 616 | let next = u16try_from.unwrap_or; |
| 617 | self.scroll.insert; |
| 618 | |
| 619 | |
| 620 | /// Hold a region at this offset. |
| 621 | |
| 622 | self.scroll.insert; |
| 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 | |
| 631 | |
| 632 | match self.shown.get |
| 633 | Some => Some, |
| 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 | |
| 647 | let count = slot.body.len; |
| 648 | if count == 0 |
| 649 | return; |
| 650 | |
| 651 | let at = match self.shown |
| 652 | Some => .rem_euclid as usize, |
| 653 | None => 0, |
| 654 | ; |
| 655 | self.shown.insert; |
| 656 | |
| 657 | |
| 658 | /// Show a particular child of a region. |
| 659 | |
| 660 | self.shown.insert; |
| 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 | |
| 671 | |
| 672 | self.opened.get.copied.unwrap_or |
| 673 | |
| 674 | |
| 675 | /// Fold an open branch, or open a shut one. |
| 676 | |
| 677 | let open = self.open; |
| 678 | self.opened.insert; |
| 679 | |
| 680 | |
| 681 | /// Whether this value is ticked. |
| 682 | |
| 683 | |
| 684 | self.ticked.contains |
| 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 | |
| 693 | if !self.ticked.remove |
| 694 | self.ticked.insert; |
| 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 | |
| 704 | self.ticked.iter.map |
| 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 | |
| 714 | self.ticked = cratespots |
| 715 | .iter |
| 716 | .filter_map |
| 717 | Row |
| 718 | ticked: Some, |
| 719 | value: Some, |
| 720 | .. |
| 721 | => Some, |
| 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 | |
| 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 = 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 | |
| 755 | |
| 756 | let mut params = new; |
| 757 | for name in names |
| 758 | let Some = spots |
| 759 | .iter |
| 760 | .filter_map |
| 761 | .find |
| 762 | else |
| 763 | continue; |
| 764 | ; |
| 765 | let value = self.typed; |
| 766 | if matches! |
| 767 | && value != SELECTED |
| 768 | |
| 769 | continue; |
| 770 | |
| 771 | params.insert; |
| 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 | |
| 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: = |
| 793 | cratereaches_chromed |
| 794 | .into_iter |
| 795 | .map |
| 796 | .collect; |
| 797 | // The caret, though, is an index into what is on the screen. |
| 798 | let hidden = cratehidden; |
| 799 | let stops = |
| 800 | cratereaches_chromed.len; |
| 801 | let live: = spots |
| 802 | .iter |
| 803 | .filter_map |
| 804 | .map |
| 805 | .collect; |
| 806 | self.edits.retain; |
| 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: = spots |
| 816 | .iter |
| 817 | .filter_map |
| 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 | Repeat => Some, |
| 824 | _ => None, |
| 825 | |
| 826 | .collect; |
| 827 | self.slots |
| 828 | .retain; |
| 829 | let was = self.focus; |
| 830 | self.focus = self.focus.min; |
| 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 |