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. `d52884b0`, decided 2026-08-12. It belongs on |
| 40 | //! this list by nature, and it is absent because no described screen |
| 41 | //! needs it yet: the one measured consumer is goingson's `search.js`, whose |
| 42 | //! completion list depends on which token the caret is inside, and that file |
| 43 | //! stays JS. Saying so here keeps the boundary explicit, so the next screen that |
| 44 | //! wants caret-dependent completion knows this is where it would land rather |
| 45 | //! than re-asking whether a description should carry one. It should not. |
| 46 | |
| 47 | use ; |
| 48 | |
| 49 | use makeover_layout as layout; |
| 50 | use ; |
| 51 | |
| 52 | use crate; |
| 53 | |
| 54 | /// What the user has done to a screen since it arrived. |
| 55 | /// |
| 56 | /// A host makes one beside the screen it is holding and keeps the two together. |
| 57 | /// Empty is the honest starting state and it draws exactly what the description |
| 58 | /// says, which is what every test that predates this passes. |
| 59 | |
| 60 | |
| 61 | /// What has been typed, by [`Field::name`](quasi_router::Field::name). |
| 62 | /// |
| 63 | /// Absent means untouched, which is different from present and empty: one |
| 64 | /// draws the description's value and the other draws a box the user has |
| 65 | /// cleared. |
| 66 | edits: , |
| 67 | /// Which reached thing has focus, as an index into [`crate::focus::spots`]. |
| 68 | /// |
| 69 | /// Focus is this renderer's and lives here rather than in a description, |
| 70 | /// which is why it survives a redraw: reach is recomputed from the screen, |
| 71 | /// focus is a fact about where the user has walked. See |
| 72 | /// [`crate::focus`]'s header for the three terms. |
| 73 | focus: usize, |
| 74 | /// How far each region has been scrolled, in rows, by |
| 75 | /// [`Slot::id`](quasi_router::Slot::id). |
| 76 | scroll: , |
| 77 | /// What has been ticked, by [`Row::value`](quasi_router::Row::value). |
| 78 | /// |
| 79 | /// `5f2b8753`. The fifth thing the browser was quietly providing, and it |
| 80 | /// arrived last because it is the one a browser does *not* fully provide: |
| 81 | /// a checkbox owns its own checked state, but nothing gathers the boxes |
| 82 | /// back up, so every app wrote that part by hand. Here there is no |
| 83 | /// checkbox to own anything, which is what made the hole visible. |
| 84 | /// |
| 85 | /// A set rather than a map from name to bool. Absent is not ticked, and |
| 86 | /// the two spellings of that would otherwise drift. |
| 87 | /// |
| 88 | /// Only the current screen's set, because a screen names one |
| 89 | /// ([`Screen::selection`](quasi_router::Screen::selection)) and a new |
| 90 | /// screen is a new set. Which set it is does not need storing: the screen |
| 91 | /// beside this one says. |
| 92 | ticked: , |
| 93 | |
| 94 | |
| 95 | |
| 96 | /// Nothing typed, the first thing focused, nothing scrolled. |
| 97 | |
| 98 | |
| 99 | Selfdefault |
| 100 | |
| 101 | |
| 102 | /// What is in the box: what has been typed, or what the description offers, |
| 103 | /// or nothing. |
| 104 | /// |
| 105 | /// The order is the whole of the type's job. An untouched field shows what |
| 106 | /// the handler put there; a touched one shows what the user did, including |
| 107 | /// when what they did was empty it. |
| 108 | |
| 109 | |
| 110 | self.showing |
| 111 | |
| 112 | |
| 113 | /// [`typed`](Self::typed) for a caller holding the described field itself |
| 114 | /// rather than a walk's record of it, which is what the drawing has. |
| 115 | |
| 116 | |
| 117 | self.edits |
| 118 | .get |
| 119 | .map |
| 120 | .or |
| 121 | .unwrap_or_default |
| 122 | |
| 123 | |
| 124 | /// What has been typed into a field by name, if anything has. |
| 125 | |
| 126 | |
| 127 | self.edits.get.map |
| 128 | |
| 129 | |
| 130 | /// Put a value in a box. |
| 131 | |
| 132 | self.edits.insert; |
| 133 | |
| 134 | |
| 135 | /// Add a character to a box, starting from whatever is showing in it. |
| 136 | |
| 137 | let mut value = self.typed.to_string; |
| 138 | value.push; |
| 139 | self.set; |
| 140 | |
| 141 | |
| 142 | /// Take the last character back out of a box. |
| 143 | |
| 144 | let mut value = self.typed.to_string; |
| 145 | value.pop; |
| 146 | self.set; |
| 147 | |
| 148 | |
| 149 | /// Which reachable thing has focus. |
| 150 | |
| 151 | pub const |
| 152 | self.focus |
| 153 | |
| 154 | |
| 155 | /// Move focus by `steps`, wrapping at both ends. |
| 156 | /// |
| 157 | /// Wrapping rather than stopping, because a terminal has no scrollbar to |
| 158 | /// tell you that you are at the end of the reachable things and pressing tab |
| 159 | /// against a dead stop reads as a broken key. |
| 160 | |
| 161 | if reachable == 0 |
| 162 | self.focus = 0; |
| 163 | return; |
| 164 | |
| 165 | let count = reachable as isize; |
| 166 | let at = self.focus.min as isize; |
| 167 | self.focus = .rem_euclid as usize; |
| 168 | |
| 169 | |
| 170 | /// Focus something in particular, if it is there. |
| 171 | |
| 172 | if at < reachable |
| 173 | self.focus = at; |
| 174 | |
| 175 | |
| 176 | |
| 177 | /// How far a region has been scrolled. |
| 178 | |
| 179 | |
| 180 | self.scroll.get.copied.unwrap_or |
| 181 | |
| 182 | |
| 183 | /// Scroll a region, never above its top. |
| 184 | /// |
| 185 | /// There is no bottom stop here, and that is deliberate: how far a region |
| 186 | /// can scroll is how tall its content is at the width it was given, which |
| 187 | /// is a fact the drawing knows and this does not. [`crate::Tui::clamp`] is |
| 188 | /// where it gets trimmed, once per draw, with the rect in hand. |
| 189 | |
| 190 | let at = i32from; |
| 191 | let next = u16try_from.unwrap_or; |
| 192 | self.scroll.insert; |
| 193 | |
| 194 | |
| 195 | /// Hold a region at this offset. |
| 196 | |
| 197 | self.scroll.insert; |
| 198 | |
| 199 | |
| 200 | /// Whether this value is ticked. |
| 201 | |
| 202 | |
| 203 | self.ticked.contains |
| 204 | |
| 205 | |
| 206 | /// Tick it if it is not, untick it if it is. |
| 207 | /// |
| 208 | /// Staging, never a write. Wiki `explicit-commit-affordance`: the commit |
| 209 | /// control is what locks a change in, and a tick that wrote on its own |
| 210 | /// would be the change happening with nothing to mark it. |
| 211 | |
| 212 | if !self.ticked.remove |
| 213 | self.ticked.insert; |
| 214 | |
| 215 | |
| 216 | |
| 217 | /// Everything ticked, in order. |
| 218 | /// |
| 219 | /// Ordered because it is a `BTreeSet`, and that is worth relying on: a |
| 220 | /// handler reading [`Params::get_all`] gets the same sequence every run, so |
| 221 | /// a test over a bulk action is not sorting the answer first. |
| 222 | |
| 223 | self.ticked.iter.map |
| 224 | |
| 225 | |
| 226 | /// Start the described ticks off, for the rows that arrive already ticked. |
| 227 | /// |
| 228 | /// A description can say a row is ticked, and on a screen that has just |
| 229 | /// arrived that claim is the only thing there is. Applied on arrival rather |
| 230 | /// than read on every draw, because after that the user's ticks are the |
| 231 | /// truth and a redraw that went back to the description would undo them. |
| 232 | |
| 233 | self.ticked = cratespots |
| 234 | .iter |
| 235 | .filter_map |
| 236 | Row |
| 237 | ticked: Some, |
| 238 | value: Some, |
| 239 | .. |
| 240 | => Some, |
| 241 | _ => None, |
| 242 | |
| 243 | .collect; |
| 244 | |
| 245 | |
| 246 | /// Forget everything typed and scrolled, and go back to the top. |
| 247 | /// |
| 248 | /// What a whole new screen means. The boxes on it are different boxes, and |
| 249 | /// carrying a buffer across would put what was typed into a password field |
| 250 | /// into whatever field happens to share its name on the next screen. A |
| 251 | /// selection goes the same way and for the same reason: the rows are |
| 252 | /// different rows. |
| 253 | |
| 254 | self.edits.clear; |
| 255 | self.scroll.clear; |
| 256 | self.ticked.clear; |
| 257 | self.focus = 0; |
| 258 | |
| 259 | |
| 260 | /// The values a form submits, gathered for `names` in the order given. |
| 261 | /// |
| 262 | /// A checkbox is here by presence, the way HTML submits one, so a box that |
| 263 | /// is not ticked sends nothing rather than sending an empty string. That is |
| 264 | /// [`Field::value`](quasi_router::Field::value)'s own convention read back |
| 265 | /// out. |
| 266 | |
| 267 | |
| 268 | let mut params = new; |
| 269 | for name in names |
| 270 | let Some = spots |
| 271 | .iter |
| 272 | .filter_map |
| 273 | .find |
| 274 | else |
| 275 | continue; |
| 276 | ; |
| 277 | let value = self.typed; |
| 278 | if matches! |
| 279 | && value != SELECTED |
| 280 | |
| 281 | continue; |
| 282 | |
| 283 | params.insert; |
| 284 | |
| 285 | params |
| 286 | |
| 287 | |
| 288 | /// Drop anything held for a field the screen no longer has. |
| 289 | /// |
| 290 | /// A fragment can replace a region holding half a form, and the buffers for |
| 291 | /// the fields that went away would otherwise ride along and be submitted by |
| 292 | /// the next form that happens to name one of them. |
| 293 | |
| 294 | let spots = cratespots; |
| 295 | let live: = spots |
| 296 | .iter |
| 297 | .filter_map |
| 298 | .map |
| 299 | .collect; |
| 300 | self.edits.retain; |
| 301 | self.focus = self.focus.min; |
| 302 | |
| 303 | |
| 304 |