Skip to main content

max / quasi

104.7 KB · 2321 lines History Blame Raw
1 //! The half a webview host never writes.
2 //!
3 //! `quasi-axum` and `quasi-tauri` both answer a request with markup and stop.
4 //! Everything between one request and the next — which control is under the
5 //! caret, what the user has typed into it, what a key means, where the back
6 //! button goes — is the browser's, and neither adapter contains a line of it.
7 //! A terminal has no browser under it, so this is that half, written out.
8 //!
9 //! # It does not own the router
10 //!
11 //! [`Runtime`] turns keys into [`Request`]s and applies [`Response`]s, and it
12 //! never calls a handler. The host holds the router and the state and does the
13 //! calling, which keeps this free of the state type and makes every binding
14 //! below testable without standing up an app.
15 //!
16 //! ```text
17 //! key ──► Runtime::key ──► Step::Call(request)
18 //!
19 //! host: router.handle(&state, request)
20 //!
21 //! Runtime::apply ◄── Response
22 //! ```
23 //!
24 //! # The bindings are this renderer's, and the description reaches two of them
25 //!
26 //! Nothing in a description says what Tab does, so the table below is policy.
27 //! The two exceptions are the two the vocabulary already carries: [`Act::key`]
28 //! names the key that reaches a control, and [`Act::confirm`] names the question
29 //! to ask before doing it. Both were drawn and declined by the drawing half,
30 //! and this is where they are honoured.
31 //!
32 //! | Key | What it does |
33 //! |---|---|
34 //! | Tab, Down | the next reachable thing |
35 //! | `BackTab`, Up | the previous one |
36 //! | Enter | call what is under the caret |
37 //! | Space | tick the row under the caret |
38 //! | `PageUp`, `PageDown` | scroll the region the caret is in |
39 //! | Backspace | take a character back out of a field |
40 //! | printable | type into a field, or reach the control that named the key |
41 //! | Escape | step out of a table row, dismiss the overlay, or go back |
42 //! | Left, Right | step into the table row under the caret and cycle its controls |
43 //!
44 //! # Right steps into a table row
45 //!
46 //! A table row is one stop with the controls in its cells inside it rather
47 //! than beside it, which [`crate::focus`]'s header explains; this is the key
48 //! that gets at them. Right steps in and cycles forward, Left cycles back,
49 //! Escape steps out, and Enter presses whatever the caret has stepped onto
50 //! instead of opening the row.
51 //!
52 //! Right and Left rather than Enter, because Enter is taken: `Row::activate`
53 //! already means "this row opens", and every measured table with a control in a
54 //! cell also opens its rows. They are the keys a grid is walked with everywhere
55 //! else, and the reader is already holding them for the same shape.
56 //!
57 //! They mean this before they mean the other thing they mean. A region showing
58 //! one child at a time is moved with the same two keys, and the caret standing
59 //! on a row with controls in it wins, because the thing under the caret beats
60 //! the screen-wide search that arm falls back to. A screen with a table inside a
61 //! carousel therefore steps between frames from any stop but that one, which is
62 //! the cost, and it is the smaller of the two: the frames are reachable from
63 //! anywhere else on the screen and the controls in the row are reachable from
64 //! nowhere else.
65
66 use makeover_layout as layout;
67 use quasi_router::{
68 Accepted, Action, Choosing, Chrome, Consult, Frame, Locating, Message, Method, Node, Outcome,
69 Params, Request, Response, Screen, Slot, safe_file_name,
70 };
71 use ratatui::buffer::Buffer;
72 use ratatui::layout::Rect;
73
74 use crate::focus::{FieldSpot, Reach, Spot};
75 use crate::{Tui, View};
76
77 /// A key, named the way this crate wants to talk about one.
78 ///
79 /// Not crossterm's, deliberately. A host maps its own events onto this in a
80 /// dozen lines, and in exchange the bindings below are testable without a
81 /// terminal and this crate does not make every consumer take a backend it might
82 /// not be using.
83 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
84 pub enum Key {
85 /// A character the user typed.
86 Char(char),
87 /// Confirm, follow, submit.
88 Enter,
89 /// Forward through the reachable things.
90 Tab,
91 /// Backward through them.
92 BackTab,
93 /// Take a character back.
94 Backspace,
95 /// Out, back, never mind.
96 Escape,
97 /// Up one reachable thing.
98 Up,
99 /// Down one reachable thing.
100 Down,
101 /// A screen's worth backwards.
102 PageUp,
103 /// A screen's worth forwards.
104 PageDown,
105 /// Back one child, in a region showing one at a time.
106 Left,
107 /// On one child, in a region showing one at a time.
108 Right,
109 }
110
111 /// How a press was meant, as the payload it travels in.
112 ///
113 /// A terminal has no modifiers on this crate's `Key`, so the two readings it
114 /// can offer are the two keys it binds: Enter is the plain one and Space is
115 /// "and also this one". `Choosing::Through` -- a run from the app's pointer to
116 /// here -- has no key yet and is not invented one: shift-Enter is not
117 /// expressible while `Key` carries no modifiers, and widening `Key` is a
118 /// change to every host that drives this renderer.
119 fn meant(choosing: Choosing) -> Params {
120 Params::new().with(Node::CHOOSING.to_owned(), choosing.as_str().to_owned())
121 }
122
123 /// What the host should do about a key.
124 #[derive(Debug, Clone, PartialEq, Eq)]
125 pub enum Step {
126 /// Nothing left to do but redraw.
127 Idle,
128 /// Ask the router this, then hand the answer to [`Runtime::apply`].
129 Call(Request),
130 /// Ask this question. The next key answers it: `y` or Enter does the thing,
131 /// anything else does not.
132 Ask(String),
133 /// Somewhere outside the app. The host opens it, and nothing comes back.
134 Open(String),
135 /// This value goes on the clipboard. The host puts it there, and nothing
136 /// comes back.
137 ///
138 /// [`Act::copies`] says a press copies something, and this is the
139 /// terminal's half. [`Open`](Self::Open)'s shape and for its reason: this
140 /// crate turns keys into requests and owns no I/O, so writing OSC 52 or
141 /// reaching a system clipboard is the host's, exactly as opening a URL is.
142 ///
143 /// A copying act's action is local -- [`Act::copying`] makes it so -- and
144 /// this is what the press produces, so nothing is asked and nothing is
145 /// lost. An act that copies *and* names a route is not describable through
146 /// this runtime in one step: the copy is what comes back and the call does
147 /// not happen. No measured site is one.
148 ///
149 /// [`Act::copies`]: quasi_router::Act::copies
150 /// [`Act::copying`]: quasi_router::Act::copying
151 Copy(String),
152 /// Ask the router these, each not until its own `after` has passed with no
153 /// further key. A later one of these replaces an outstanding one whole
154 /// rather than queueing behind it.
155 ///
156 /// [`Field::consults`], and the waits are the description's own. Until it
157 /// existed this runtime had no way to say "later" and the comment on
158 /// [`Runtime::after_typing`] said why it would not invent one: a delay
159 /// picked here is a delay the webview renderer disagrees with, and the two
160 /// hosts then debounce the same field differently.
161 ///
162 /// The clock stays with the host, exactly as the router does. This crate
163 /// turns keys into requests and owns no I/O, so "wait 500ms, and drop this
164 /// if another key arrives" is the host's timer to run — the same division
165 /// that keeps [`Call`](Self::Call) from calling anything.
166 ///
167 /// # Several, and still no scheduler here
168 ///
169 /// `N8`. One box can raise more than one question: MNW's discover search
170 /// asks a suggestion route after 200ms and a results route after 150. The
171 /// runtime said it did not want a queue and still does not have one — this
172 /// is one keystroke producing one `Step`, and what varies is how many
173 /// questions that keystroke posed. The host runs a timer per entry, which
174 /// is the timer it was already running, and replacement stays wholesale:
175 /// every question here is about the same value, so a newer keystroke
176 /// invalidates all of them together.
177 ///
178 /// [`Field::consults`]: quasi_router::Field::consults
179 CallAfter {
180 /// The questions this keystroke posed, in the order the description
181 /// asked them.
182 asks: Vec<Delayed>,
183 },
184 }
185
186 /// One question and how long the value must stand still before it is asked.
187 ///
188 /// [`Step::CallAfter`]'s member. A pair rather than two parallel lists, so a
189 /// host cannot pick up a request and the wrong wait.
190 #[derive(Debug, Clone, PartialEq, Eq)]
191 pub struct Delayed {
192 /// What to ask.
193 pub request: Request,
194 /// How long the value must stand still first.
195 pub after: std::time::Duration,
196 }
197
198 /// A file a route answered with, for the host to put somewhere.
199 ///
200 /// [`Outcome::File`] says what the file is and never where it goes, so this
201 /// runtime does not write it: this crate turns keys into requests and owns no
202 /// I/O, the same division that keeps [`Step::Call`] from calling anything and
203 /// [`Step::Open`] from opening anything.
204 ///
205 /// The host drains it with [`Runtime::handed`] after [`Runtime::apply`]. On a
206 /// terminal the ruling says what to do with it — write it into the working
207 /// directory under [`name`](Self::name) — and a host with somewhere better to
208 /// put it is free to ask.
209 #[derive(Debug, Clone, PartialEq, Eq)]
210 pub struct Handed {
211 /// The suggested file name, already through [`safe_file_name`].
212 ///
213 /// Sanitised here rather than by each host, because the host that forgets
214 /// is the one that writes `../../.ssh/authorized_keys` beside the process.
215 pub name: String,
216 /// What kind of file it is.
217 pub kind: Accepted,
218 /// The file.
219 pub bytes: Vec<u8>,
220 }
221
222 /// How the layer on top is drawn over the one under it.
223 ///
224 /// The two outcomes that layer a screen want different boxes, and this is the
225 /// whole of the difference between them here: everything else -- the stack,
226 /// the dedupe, the dismissal, the keys -- is one code path.
227 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
228 pub(crate) enum Laid {
229 /// Centred and inset on all four sides. `Outcome::Over`: a palette, a help
230 /// overlay, an app-modal dialog, all of which are about the app rather than
231 /// about anything on the screen.
232 Over,
233 /// A compact box in the half of the screen the subject is not in.
234 /// `Outcome::Anchored`.
235 ///
236 /// # Why a half and not a position
237 ///
238 /// This renderer has no coordinates to anchor to. `makeover_tui::table`
239 /// decides its own column widths and answers none back, and
240 /// `crate::focus`' header says why per-cell rects are not being added: Max
241 /// ruled against them. So a terminal cannot draw a menu *at* the control
242 /// the way egui can, and inventing a cell to point at would be this
243 /// renderer guessing.
244 ///
245 /// What it can promise is that the menu does not cover what it was opened
246 /// on. The reach order is the one ordering this renderer does have, so a
247 /// subject in the first half of it puts the menu low and one in the second
248 /// half puts it high. That is proximity said in the only currency a
249 /// terminal has, and it is honest about being a half rather than a point.
250 Anchored {
251 /// Whether the menu sits in the lower half.
252 low: bool,
253 },
254 }
255
256 /// A screen and what the user has done to it.
257 ///
258 /// The pair is the unit an overlay needs: an overlay has its own reach, its own
259 /// focus, its own edits and its own scroll, so it holds a [`View`] of its own
260 /// rather than borrowing the one underneath. Sharing it is the bug the overlay
261 /// tests exist to catch — dismissing a palette would take the user's typing and
262 /// scroll position with it.
263 #[derive(Debug, Clone)]
264 struct Layer {
265 screen: Screen,
266 view: View,
267 /// The request that opened the overlay this layer was displaced by.
268 ///
269 /// Saved on the way down and restored on the way up, so a nested overlay
270 /// dismissing back to an outer one restores the *outer* one's identity
271 /// rather than losing it. See [`Runtime::over`].
272 over: Option<Request>,
273 /// How the layer this one was displaced by was drawn. Saved and restored
274 /// with `over`, and for its reason. See [`Runtime::laid`].
275 laid: Option<Laid>,
276 }
277
278 /// A screen, what the user has done to it, and how they got here.
279 #[derive(Debug, Clone)]
280 pub struct Runtime {
281 screen: Screen,
282 view: View,
283 /// What the app offers from every screen. Matched before this runtime's own
284 /// key table, so a screen cannot capture the key that opens the palette.
285 chrome: Chrome,
286 /// The layers this one is drawn over, outermost first.
287 ///
288 /// Empty on an ordinary screen. `screen` and `view` above are always the
289 /// ACTIVE layer, so every key, every gather and every draw works on the
290 /// overlay once one is open, with no second code path.
291 ///
292 /// Separate from `history` on purpose: an overlay is not a place. Opening
293 /// one pushes here and leaves history alone, and dismissing one pops here
294 /// and reveals the screen the user never left.
295 under: Vec<Layer>,
296 /// The places behind this one, most recent last.
297 ///
298 /// Requests rather than addresses, because going back means asking again
299 /// and a request is what asking takes. [`Address`](quasi_router::Address)
300 /// carries a string for a browser's address bar, which a terminal does not
301 /// have.
302 history: Vec<Request>,
303 /// The request that produced the screen currently showing.
304 here: Option<Request>,
305 /// The request that produced the overlay currently on top, if one is.
306 ///
307 /// Five presses of a help key were five Escapes.
308 ///
309 /// Nothing in the description had to grow for it. The runtime is handed the
310 /// request it fired, so the identity the dedupe needs is already in hand --
311 /// this is `here`'s shape for a layer that is not a place, which is why
312 /// `remember` is not called for one.
313 ///
314 /// A webview never had the bug: `Outcome::Over` lands in one overlay
315 /// container and replaces what is in it. Two of three hosts stacking was
316 /// one description behaving two ways, which is the drift this stack exists
317 /// to end.
318 over: Option<Request>,
319 /// Where the layer on top is drawn, when one is.
320 ///
321 /// An `Outcome::Over` is a palette and takes the inset box in the middle;
322 /// an `Outcome::Anchored` is a menu and takes a compact box out of the
323 /// subject's way. Kept beside `over` rather than on `Layer`, because it is
324 /// a fact about the layer on top and `Layer` holds the ones underneath.
325 ///
326 /// `None` whenever `over` is, and the two move together.
327 pub(crate) laid: Option<Laid>,
328 /// A control waiting on its own question being answered.
329 asked: Option<(Action, Params)>,
330 /// The awaiting call this runtime has dispatched and not yet been answered
331 /// about, with the request that went out.
332 ///
333 /// Only an action carrying [`Action::awaiting`] lands here: the
334 /// description is what says which calls are worth locking a control for,
335 /// so this renderer does not decide that a route is slow.
336 ///
337 /// The pair rather than the action alone, because the refusal is against the
338 /// request that is outstanding: pressing the same control again is the
339 /// double submit, and pressing a different one is a different call this has
340 /// no business refusing.
341 outstanding: Option<(Action, Request)>,
342 /// Something to say once the screen it belongs to has arrived.
343 saying: Option<Message>,
344 /// What this mount puts around whatever screen is showing.
345 ///
346 /// Held beside the screen rather than arriving with one, which is what
347 /// makes it the mount's: it survives every answer that replaces the screen
348 /// inside it, the way `chrome` above survives every navigation. The
349 /// difference between the two is lifetime — chrome is the app's and this
350 /// is one place the app puts a screen up.
351 frame: Frame,
352 /// When this last handed out [`Runtime::refreshes`], so the cadence is kept
353 /// here rather than by every host that draws a live screen.
354 ///
355 /// `None` until the first call, which makes the first ask immediate: a
356 /// region that waited out a whole period before its first answer would be a
357 /// slower screen than the one liveness replaces.
358 refreshed: Option<std::time::Instant>,
359 /// When each toast on the screen was raised, in the order the toasts sit in
360 /// `screen.notices`.
361 ///
362 /// The description says a toast goes away on its own and never says when,
363 /// so the when is kept here: one instant per transient notice, and
364 /// [`expires_at`](Runtime::expires_at) takes away the ones whose time is
365 /// up. Banners have no entry, because nothing about a banner is on a
366 /// clock.
367 ///
368 /// Positional rather than keyed, because a notice has no identity to key
369 /// on and does not need one: a toast joins the screen at the end of the
370 /// list ([`announce`](Runtime::announce)) or arrives inside a whole screen,
371 /// and both are handled where they happen rather than guessed at here.
372 raised: Vec<std::time::Instant>,
373 /// A file a route answered with and the host has not taken yet.
374 ///
375 /// Drained by [`handed`](Runtime::handed) rather than returned from
376 /// [`apply`](Runtime::apply), because a file and a follow-up request are
377 /// not alternatives: an answer is one or the other, and widening `apply`'s
378 /// return type to say so would change every host for the case that cannot
379 /// happen.
380 ///
381 /// One at a time, and a second overwrites the first. Two files from one
382 /// answer is not a thing the vocabulary can say.
383 handed: Option<Handed>,
384 /// A place a route asked for and the host has not gone looking for yet.
385 ///
386 /// [`handed`](Self::handed)'s twin, drained by
387 /// [`locating`](Runtime::locating) and here for the same reason: this crate
388 /// turns keys into requests and owns no I/O, and asking a person where to
389 /// write is I/O of the most blocking kind.
390 ///
391 /// One at a time, and a second replaces the first. A picker is modal
392 /// wherever there is one, and on a terminal there is nothing to open at
393 /// all — see [`locating`](Runtime::locating) for what this host does with
394 /// one instead.
395 locating: Option<Locating>,
396 }
397
398 impl Runtime {
399 /// Start on this screen, with nothing typed and nothing behind it.
400 #[must_use]
401 pub fn new(screen: Screen) -> Self {
402 let mut runtime = Self {
403 screen,
404 view: View::new(),
405 chrome: Chrome::new(),
406 under: Vec::new(),
407 laid: None,
408 history: Vec::new(),
409 here: None,
410 over: None,
411 asked: None,
412 outstanding: None,
413 saying: None,
414 frame: Frame::new(),
415 refreshed: None,
416 raised: Vec::new(),
417 handed: None,
418 locating: None,
419 };
420 runtime.view.seed(&runtime.screen);
421 runtime.open_caret();
422 runtime.reraise(std::time::Instant::now());
423 runtime
424 }
425
426 /// Declare what the app offers from every screen.
427 ///
428 /// Held beside the screen rather than arriving with one, which is what
429 /// makes it chrome: the bindings outlive every answer this runtime applies.
430 #[must_use]
431 pub fn with_chrome(mut self, chrome: Chrome) -> Self {
432 self.chrome = chrome;
433 self
434 }
435
436 /// Declare what this mount puts around the screen.
437 ///
438 /// A mount with two ways of showing one screen builds two runtimes
439 /// carrying two frames, which is what it already does for everything else
440 /// it holds across frames. The screen inside them says nothing about
441 /// either.
442 #[must_use]
443 pub fn with_frame(mut self, frame: Frame) -> Self {
444 self.frame = frame;
445 self
446 }
447
448 /// What the app offers from every screen.
449 ///
450 /// The panel is what a caller reads off this: an answer aimed at it lands
451 /// here rather than on the screen, so this is where its current contents
452 /// are.
453 #[must_use]
454 pub const fn chrome(&self) -> &Chrome {
455 &self.chrome
456 }
457
458 /// What this mount puts around the screen.
459 #[must_use]
460 pub const fn frame(&self) -> &Frame {
461 &self.frame
462 }
463
464 /// Whether an overlay is open over the screen.
465 #[must_use]
466 pub const fn overlaid(&self) -> bool {
467 !self.under.is_empty()
468 }
469
470 /// The screen being shown.
471 ///
472 /// The overlay's, when one is open. That is what "being shown" means, and
473 /// it is what every key this runtime handles is working on.
474 #[must_use]
475 pub const fn screen(&self) -> &Screen {
476 &self.screen
477 }
478
479 /// What the user has done to it.
480 #[must_use]
481 pub const fn view(&self) -> &View {
482 &self.view
483 }
484
485 /// The request that produced the screen showing now.
486 ///
487 /// `None` before the first navigation: a runtime is built from a screen
488 /// rather than from an address, so the opening screen has no request behind
489 /// it until the host performs one.
490 #[must_use]
491 pub const fn here(&self) -> Option<&Request> {
492 self.here.as_ref()
493 }
494
495 /// Ask for this screen again.
496 ///
497 /// **What a described screen has no other way to say: the thing it is about
498 /// changed, and nothing the user did to this screen changed it.** A route
499 /// answers a screen built from the state at the moment it was asked, and
500 /// that answer is kept until something fires. So a host whose state moves
501 /// underneath a screen — a background job reporting progress, a write the
502 /// host applies after the frame — has a screen describing a past it can
503 /// neither notice nor correct.
504 ///
505 /// This is the host saying so. It is deliberately not a description member:
506 /// nothing in [`Screen`] claims a refresh rate, because how often a fact
507 /// goes stale is a property of the app holding it rather than of the screen
508 /// showing it.
509 ///
510 /// [`Step::Idle`] when there is nothing to ask for, which is the opening
511 /// screen before any navigation. History is untouched: asking for the screen
512 /// you are on again is not going anywhere.
513 #[must_use]
514 pub fn reload(&self) -> Step {
515 match &self.here {
516 Some(request) => Step::Call(request.clone()),
517 None => Step::Idle,
518 }
519 }
520
521 /// The calls this screen's regions are waiting on, for the host to perform.
522 ///
523 /// A webview gets this for free: every region fed by a call carries a
524 /// trigger and the browser asks as soon as the element exists. A terminal
525 /// has nobody to do that, so the host asks for these after putting a screen
526 /// up, hands each answer back to [`apply`](Self::apply), and the region
527 /// fills where its stand-in was.
528 ///
529 /// Ask again after applying an answer rather than keeping the list: a
530 /// fragment landing clears the region's feed, so what comes back is what is
531 /// still outstanding.
532 #[must_use]
533 pub fn feeds(&self) -> Vec<Request> {
534 self.screen
535 .feeds()
536 .into_iter()
537 .filter_map(Self::request_for)
538 .collect()
539 }
540
541 /// The calls this screen's live regions re-ask, when it is time to ask.
542 ///
543 /// [`feeds`](Self::feeds)' counterpart and never overlapping it: a feed
544 /// arrives once and a refresh never stops. Empty until
545 /// [`CADENCE`](crate::CADENCE) has passed since the last time this handed
546 /// anything back, so a host may call it as often as it likes and the rate
547 /// stays this crate's.
548 ///
549 /// The pacing is here rather than in each host for the reason the number
550 /// is: a host that timed its own polling would be a host the other
551 /// renderers disagree with, and every app would rebuild the same timer.
552 ///
553 /// # Two shapes of answer
554 ///
555 /// A live region naming a call comes back as that call, and its answer is a
556 /// fragment for that region. A live region naming none comes back as the
557 /// screen's own address, because re-reading state the host already holds
558 /// means building the description again — and that is a whole screen, not a
559 /// fragment. Both are requests, and a host performs them the same way.
560 #[must_use]
561 pub fn refreshes(&mut self) -> Vec<Request> {
562 self.refreshes_at(std::time::Instant::now())
563 }
564
565 /// [`refreshes`](Self::refreshes) against a clock the caller holds.
566 ///
567 /// The seam a test needs, and the one an event loop that already knows what
568 /// time it is should reach for rather than asking again.
569 #[must_use]
570 pub fn refreshes_at(&mut self, now: std::time::Instant) -> Vec<Request> {
571 let due = self
572 .refreshed
573 .is_none_or(|last| now.duration_since(last) >= crate::CADENCE);
574 if !due {
575 return Vec::new();
576 }
577 let mut out: Vec<Request> = self
578 .screen
579 .refreshes()
580 .into_iter()
581 .filter_map(Self::request_for)
582 .collect();
583 // A live region that names no call is re-read by asking the screen's own
584 // address again, which is what re-reading means for a host that retains
585 // a description rather than a document. The audiofiles sync panel is
586 // that case: its state is the app's own and moves when an OAuth callback
587 // lands in another process, so there is no fragment to fetch and the
588 // whole screen is rebuilt from what is true now.
589 //
590 // Only when nothing else answered. A screen with a live region that does
591 // name a call has already been given the narrower ask, and adding the
592 // address to it would rebuild the screen the fragment was about to land
593 // in.
594 if out.is_empty()
595 && self.screen.is_live()
596 && let Some(here) = &self.here
597 {
598 out.push(here.clone());
599 }
600 // Stamped even when the screen has nothing live, so a still screen is
601 // not re-walked on every frame an egui host draws.
602 self.refreshed = Some(now);
603 out
604 }
605
606 /// Whether anything on this screen changes without the user.
607 ///
608 /// True for a live region whether or not it names a call, which is the
609 /// difference from [`refreshes`](Self::refreshes): a region reading state
610 /// the host already holds has nothing to ask for and still has to be
611 /// redrawn.
612 #[must_use]
613 pub fn is_live(&self) -> bool {
614 self.screen.is_live()
615 }
616
617 /// How long a host may wait before this screen has to be drawn again for
618 /// its own sake, if it holds anything that goes stale on its own.
619 ///
620 /// A terminal redraws on an event, and a readout derived from the current
621 /// time is the case with no event to redraw on. What comes back is the
622 /// finest cadence the screen's readouts demand, so a host blocking on
623 /// input gives this as its timeout and every readout on the screen moves
624 /// on one tick.
625 ///
626 /// A toast counts too, and its answer is what is left of its
627 /// [`LINGER`](crate::LINGER) rather than a cadence: it is drawn again once,
628 /// to not be drawn at all.
629 ///
630 /// `None` for a screen holding none, which is nearly all of them, and the
631 /// host may then block for as long as it likes. Independent of
632 /// [`refreshes`](Self::refreshes): that one asks something over a network
633 /// on [`CADENCE`](crate::CADENCE), and this is arithmetic the terminal does
634 /// itself. A screen with both takes the shorter answer for its timeout and
635 /// the two stay separate facts.
636 #[must_use]
637 pub fn tick_in(&self) -> Option<std::time::Duration> {
638 self.tick_in_at(std::time::Instant::now())
639 }
640
641 /// [`tick_in`](Self::tick_in) against a clock the caller holds.
642 ///
643 /// The seam a test needs, and the one an event loop that already knows what
644 /// time it is should reach for rather than asking again.
645 #[must_use]
646 pub fn tick_in_at(&self, now: std::time::Instant) -> Option<std::time::Duration> {
647 let clocks = self.screen.clocks().into_iter().map(crate::cadence).min();
648 let toasts = self
649 .raised
650 .iter()
651 .map(|at| crate::LINGER.saturating_sub(now.duration_since(*at)))
652 .min();
653 clocks.into_iter().chain(toasts).min()
654 }
655
656 /// Take away every toast whose time is up, and say whether one went.
657 ///
658 /// `Notice::Toast` says the message goes away on its own, and this is the
659 /// terminal keeping that promise: a browser has a timer per element and a
660 /// terminal has an event loop, so the loop asks this once a turn the way
661 /// it already asks [`refreshes`](Self::refreshes), and
662 /// [`tick_in`](Self::tick_in) is what wakes a loop that is only blocking on
663 /// a key.
664 ///
665 /// A banner is never touched. It goes when the condition it reports is
666 /// fixed, which is a route's business and not a clock's.
667 pub fn expires(&mut self) -> bool {
668 self.expires_at(std::time::Instant::now())
669 }
670
671 /// [`expires`](Self::expires) against a clock the caller holds.
672 pub fn expires_at(&mut self, now: std::time::Instant) -> bool {
673 if self.raised.is_empty() {
674 return false;
675 }
676 let raised = std::mem::take(&mut self.raised);
677 let mut ages = raised.into_iter();
678 let mut kept = Vec::new();
679 let before = self.screen.notices.len();
680 self.screen.notices.retain(|node| {
681 let Node::Notice { kind, .. } = node else {
682 return true;
683 };
684 // `eea7ba88`. One call rather than a transience check and a
685 // constant: `None` is a notice with no lifetime, which is a banner,
686 // and it is kept for the same reason the check used to keep it.
687 let Some(lifetime) = makeover_timing::notice_lifetime(kind.transient()) else {
688 return true;
689 };
690 // A toast with no instant beside it is one this runtime never saw
691 // raised, which nothing in the crate produces. It is given now
692 // rather than dropped: an unexplained toast on the screen is a
693 // smaller wrong than a message the user never got to read.
694 let at = ages.next().unwrap_or(now);
695 let up = now.duration_since(at) >= lifetime;
696 if !up {
697 kept.push(at);
698 }
699 !up
700 });
701 self.raised = kept;
702 self.screen.notices.len() != before
703 }
704
705 /// Start every toast on the screen lingering from now.
706 ///
707 /// A whole screen arriving brings whatever notices it was described with,
708 /// and a screen coming back out from under an overlay is in front of the
709 /// user again. Both are the moment the reading starts, so both reset the
710 /// clock rather than trying to remember one from before.
711 fn reraise(&mut self, now: std::time::Instant) {
712 let toasts = self
713 .screen
714 .notices
715 .iter()
716 .filter(|node| matches!(node, Node::Notice { kind, .. } if kind.transient()))
717 .count();
718 self.raised = vec![now; toasts];
719 }
720
721 /// What the control that was pressed is waiting on, if one is.
722 ///
723 /// The amount rides on it when the description measured one, which is what
724 /// a host drawing its own progress reads. Nothing here turns it into a time:
725 /// a bar shows what is done over what there is and how long it has taken,
726 /// and predicts nothing.
727 #[must_use]
728 pub fn awaiting(&self) -> Option<layout::Awaiting> {
729 self.outstanding
730 .as_ref()
731 .and_then(|(action, _)| action.awaiting)
732 }
733
734 /// Everything reachable on it, in focus order.
735 ///
736 /// The regions that do not apply right now are left out, which is what the
737 /// drawing does with them: the caret stops on what is on the screen.
738 #[must_use]
739 pub fn reaches(&self) -> Vec<Reach> {
740 let hidden = crate::reveal::hidden(&self.screen, &self.chrome, &self.view);
741 crate::focus::reaches_chromed(
742 &self.screen,
743 &self.frame,
744 &self.chrome,
745 &crate::Local::of(&hidden, &self.view),
746 )
747 }
748
749 /// Put the caret where the screen said it starts.
750 ///
751 /// Arrival behaviour and nothing else, which is the whole of what
752 /// [`Screen::opens_at`] is: a screen cannot pull the caret back on a
753 /// redraw, so this runs where [`View::seed`] runs and nowhere else. A
754 /// screen that names nothing leaves the caret at the first stop, which is
755 /// where a new screen has always started.
756 ///
757 /// A name no field on the screen carries moves nothing. Not an error, for
758 /// the reason the member states: the screen is the app's and so is the
759 /// name.
760 fn open_caret(&mut self) {
761 let Some(name) = self.screen.opens_at.clone() else {
762 return;
763 };
764 let reaches = self.reaches();
765 let Some(at) = reaches
766 .iter()
767 .position(|reach| reach.spot.field().is_some_and(|field| field.name == name))
768 else {
769 return;
770 };
771 self.view.focus_on(at, reaches.len());
772 }
773
774 /// Everything the screen describes, whether or not it is out right now.
775 ///
776 /// What a submit reads.: a region that does not apply is left off the
777 /// screen here and its boxes still hold what was typed into them, exactly
778 /// as a browser keeps the value of a hidden input and sends it. The caret
779 /// walk and this one differ only in that, and each is used where it is the
780 /// honest answer.
781 fn described(&self) -> Vec<Reach> {
782 crate::focus::reaches_chromed(
783 &self.screen,
784 &self.frame,
785 &self.chrome,
786 &crate::Local::of(&crate::Hidden::none(), &self.view),
787 )
788 }
789
790 /// Whether the caret is in a field, which is what decides whether a
791 /// printable key is a shortcut or a character.
792 ///
793 /// A host wanting `q` to quit asks this first. Quitting is the host's and
794 /// not a binding here, because a key that closes the app is a fact about
795 /// the app rather than about the screen.
796 #[must_use]
797 pub fn editing(&self) -> bool {
798 self.focused().is_some_and(|spot| spot.field().is_some())
799 }
800
801 /// Whether a question is waiting to be answered.
802 #[must_use]
803 pub const fn asking(&self) -> bool {
804 self.asked.is_some()
805 }
806
807 /// Draw it.
808 pub fn draw(&self, tui: &Tui, area: Rect, buf: &mut Buffer) {
809 // What is under it first, outermost first, then this one over the top.
810 // An overlay that painted only itself would be a screen swap wearing
811 // another name.
812 for layer in &self.under {
813 tui.screen(&layer.screen, &layer.view, area, buf);
814 }
815 let area = if self.under.is_empty() {
816 area
817 } else {
818 // `ae8e8836`. A palette takes the middle and a menu takes a corner
819 // out of the subject's way. `Laid::Over` is the fallback for a
820 // stack that predates the member as well as for an anchor that
821 // named nothing, which is the same box this always drew.
822 let inset = match self.laid {
823 Some(Laid::Anchored { low }) => Self::anchored_area(area, low),
824 Some(Laid::Over) | None => Self::overlay_area(area),
825 };
826 // Clear what is under it inside its own bounds, so the overlay
827 // reads as being over the screen rather than mixed into it.
828 for y in inset.top()..inset.bottom() {
829 for x in inset.left()..inset.right() {
830 buf[(x, y)].reset();
831 }
832 }
833 inset
834 };
835 tui.chromed(
836 &self.screen,
837 &self.frame,
838 &self.chrome,
839 &self.view,
840 area,
841 buf,
842 );
843 }
844
845 /// Where an overlay sits inside the screen it is over.
846 ///
847 /// Inset on all four sides so the screen underneath stays visible around
848 /// it, which is the whole visual claim an overlay makes. Proportional
849 /// rather than fixed: a palette 4 rows from the edge of an 80x24 terminal
850 /// is a different thing from one 4 rows from the edge of a 200x60.
851 fn overlay_area(area: Rect) -> Rect {
852 let pad_x = (area.width / 8)
853 .max(1)
854 .min(area.width.saturating_sub(2) / 2);
855 let pad_y = (area.height / 8)
856 .max(1)
857 .min(area.height.saturating_sub(2) / 2);
858 Rect {
859 x: area.x + pad_x,
860 y: area.y + pad_y,
861 width: area.width.saturating_sub(pad_x * 2),
862 height: area.height.saturating_sub(pad_y * 2),
863 }
864 }
865
866 /// Put a screen over the one showing, keeping that one whole underneath.
867 ///
868 /// The shared half of `Outcome::Over` and `Outcome::Anchored`, which differ
869 /// only in the box they take. Everything else about a layer -- the stack,
870 /// the dedupe, the seeding, the dismissal -- is one path, and the two
871 /// outcomes drifting apart here is what would make one description behave
872 /// two ways.
873 ///
874 /// # The dedupe
875 ///
876 /// A binding that opens an overlay is asked at the top of every frame and
877 /// an open overlay does not suppress it, so this pushed a second copy of
878 /// the same help over the first and it took an Escape per press to get
879 /// back. The guard is the request rather than the screen, because every
880 /// press is a fresh route call and the two screen values are equal by
881 /// accident rather than by identity.
882 ///
883 /// The top layer only. A screen raised from within another one is a
884 /// different request and still stacks, which is what a confirm over a
885 /// palette is.
886 fn layer(&mut self, request: &Request, screen: Screen, laid: Laid) {
887 if self.over.as_ref() == Some(request) {
888 return;
889 }
890 let under = Layer {
891 screen: std::mem::replace(&mut self.screen, screen),
892 view: std::mem::replace(&mut self.view, View::new()),
893 over: self.over.replace(request.clone()),
894 laid: self.laid.replace(laid),
895 };
896 self.under.push(under);
897 self.view.seed(&self.screen);
898 self.open_caret();
899 self.reraise(std::time::Instant::now());
900 self.announce();
901 }
902
903 /// Where an anchored menu sits inside the screen it is over.
904 ///
905 /// A compact box against one edge rather than the centred inset an overlay
906 /// takes, because a menu is a short list of verbs and a box the size of a
907 /// palette around six of them reads as a screen. Half the width and half
908 /// the height, in the half `low` names, so the subject stays visible in the
909 /// other one. See [`Laid::Anchored`] for why a half is the honest unit.
910 fn anchored_area(area: Rect, low: bool) -> Rect {
911 let width = (area.width / 2).max(1);
912 let height = (area.height / 2).max(1);
913 // Against the left edge, inset by one, so a menu and the screen under
914 // it never share a column of glyphs. The vertical half is the half the
915 // subject is not in.
916 let x = area.x + u16::from(area.width > 2);
917 let y = if low {
918 area.y + area.height.saturating_sub(height)
919 } else {
920 area.y
921 };
922 Rect {
923 x,
924 y,
925 width: width.min(area.width.saturating_sub(x - area.x)),
926 height,
927 }
928 }
929
930 /// Whether the thing the user is on sits in the first half of the reach
931 /// order, which is what puts an anchored menu low.
932 ///
933 /// The reach order is the only ordering this renderer has; see
934 /// [`Laid::Anchored`]. A screen with nothing reachable answers `false` and
935 /// the menu goes high, which is where a menu with no subject to avoid may
936 /// as well be.
937 fn subject_is_high(&self) -> bool {
938 let reach = self.reaches().len();
939 let at = self.view.focus();
940 reach > 0 && at < reach && at * 2 < reach
941 }
942
943 /// Put a message on the screen, from the host rather than from a route.
944 ///
945 /// The host has things to say that no handler knows about: a route that
946 /// failed, an address it will not open, a device that is not there. Without
947 /// this they would go to stderr, which on a terminal app is underneath the
948 /// alternate screen and therefore nowhere.
949 pub fn say(&mut self, text: impl Into<String>) {
950 self.screen.notices.push(Node::Notice {
951 kind: layout::Notice::Banner,
952 tone: layout::Tone::Danger,
953 text: text.into(),
954 // Nothing to do about it. What the host says here is a report --
955 // a route that failed, an address it will not open -- and there is
956 // no route it could offer that would undo any of them.
957 act: None,
958 });
959 }
960
961 /// What is under the caret.
962 #[must_use]
963 pub fn focused(&self) -> Option<Spot> {
964 let mut reaches = self.reaches();
965 if self.view.focus() >= reaches.len() {
966 return None;
967 }
968 Some(reaches.swap_remove(self.view.focus()).spot)
969 }
970
971 /// Take a key, and say what the host should do about it.
972 pub fn key(&mut self, key: Key) -> Step {
973 // A question owns the keyboard until it is answered. Anything that is
974 // not yes is no, which is the safe way round for a prompt that is only
975 // ever raised by something destructive.
976 if let Some((action, payload)) = self.asked.take() {
977 return match key {
978 // The selection was gathered when the question was raised, not
979 // now. Nothing can tick while a prompt owns the keyboard, so
980 // the two are the same set -- and reading it here would mean
981 // the answer depended on state the user could not see.
982 Key::Char('y' | 'Y') | Key::Enter => self.send(&action, payload),
983 _ => Step::Idle,
984 };
985 }
986
987 // The app's own keys, before this runtime's table and before any
988 // screen's `Act::key`. An affordance available everywhere is not
989 // available everywhere if a screen can capture its key.
990 //
991 // Not while typing: a field has the keyboard, and a binding on a
992 // printable key would otherwise be unreachable as a character. A
993 // binding naming a key no field can consume still lands.
994 if !self.editing() || !matches!(key, Key::Char(_)) {
995 let pressed = Self::key_name(key);
996 if let Some(binding) = pressed.as_deref().and_then(|name| self.chrome.bound(name)) {
997 return self.call(&binding.action.clone());
998 }
999 }
1000
1001 let reaches = self.reaches();
1002 let count = reaches.len();
1003 let here = reaches
1004 .get(self.view.focus())
1005 .map(|reach| reach.spot.clone());
1006 // What the caret is actually on, which is the row until it has stepped
1007 // into one. Every arm below that acts on the caret takes this, so
1008 // pressing Enter inside a row fires the control rather than opening the
1009 // row; the arms that act on the row itself take `here` and say so.
1010 let caret = match (&here, self.view.inside()) {
1011 (Some(Spot::Row { inside, .. }), Some(at)) => {
1012 inside.get(at).cloned().or_else(|| here.clone())
1013 }
1014 _ => here.clone(),
1015 };
1016
1017 // A field with an open suggestion list owns four keys while it is
1018 // open, and gives them all back the moment it closes. Arrows move the
1019 // highlight rather than the focus, Enter takes the highlighted
1020 // candidate, and Escape puts the list away before it means anything
1021 // else -- which is the innermost-thing-first rule Escape already
1022 // follows below, one level further in.
1023 //
1024 // Enter with nothing highlighted is deliberately not captured: a list
1025 // arrives with no highlight, so a form stays submittable from a box the
1026 // user has typed into and paused in.
1027 if let Some(field) = caret.as_ref().and_then(Spot::field)
1028 && self.view.suggesting(&field.name).is_some()
1029 {
1030 match key {
1031 Key::Down => {
1032 self.view.highlight(1);
1033 return Step::Idle;
1034 }
1035 Key::Up => {
1036 self.view.highlight(-1);
1037 return Step::Idle;
1038 }
1039 Key::Escape => {
1040 self.view.unsuggest();
1041 return Step::Idle;
1042 }
1043 Key::Enter => {
1044 if let Some(candidate) = self.view.pick() {
1045 // A candidate that says what picking it does has that
1046 // performed, and nothing is written into the box.
1047 // `ed1fa86f`: local is the default, not the definition.
1048 //
1049 // Performed exactly as a control's action is, with no
1050 // payload rule invented for this one place -- an action
1051 // already carries its own params and the view it was
1052 // offered under. MNW's search box is the case that
1053 // decides it: its candidates are `Action::get(url)` to
1054 // a project, item or creator page, and appending the
1055 // typed value to that address would be a description
1056 // saying something the route never asked for.
1057 if let Some(action) = candidate.picks {
1058 return self.send(&action, Params::new());
1059 }
1060 let value = candidate.value;
1061 let name = field.name.clone();
1062 self.view.set(&name, value.clone());
1063 // What a pick costs is what a settled value costs: the
1064 // webview sends `change` for the same reason, and a
1065 // field that writes when it changes has just changed.
1066 // What it does not do is re-ask the questions a field
1067 // asks *while it is being typed into*, which is why
1068 // `after_typing` is not called here.
1069 if let Some(action) = field.writes.clone() {
1070 let mut payload = Params::new();
1071 payload.insert(name, value);
1072 return self.send(&action, payload);
1073 }
1074 return Step::Idle;
1075 }
1076 }
1077 _ => {}
1078 }
1079 }
1080
1081 match key {
1082 Key::Tab | Key::Down => {
1083 let wrote = self.leaving(here.as_ref());
1084 self.view.advance(1, count);
1085 wrote.unwrap_or(Step::Idle)
1086 }
1087 Key::BackTab | Key::Up => {
1088 let wrote = self.leaving(here.as_ref());
1089 self.view.advance(-1, count);
1090 wrote.unwrap_or(Step::Idle)
1091 }
1092
1093 Key::PageDown | Key::PageUp => {
1094 // The region the caret is in, because it is the one the user is
1095 // working in. A screen with focus nowhere scrolls nothing,
1096 // which is honest: there is no "the pane" on a screen with
1097 // several.
1098 if let Some(reach) = reaches.get(self.view.focus()) {
1099 let rows = if matches!(key, Key::PageDown) {
1100 10
1101 } else {
1102 -10
1103 };
1104 self.view.scroll_by(&reach.region, rows);
1105 }
1106 Step::Idle
1107 }
1108
1109 Key::Left | Key::Right => {
1110 // A branch under the caret first. `ccaa7e4b`: these are the
1111 // keys every tree in a terminal already answers, and a reader
1112 // who has walked onto a folder presses them expecting it to
1113 // open. Right opens and Left shuts, so a press that changes
1114 // nothing is one that says the branch was already that way.
1115 //
1116 // Ahead of the two readings below because a row that is a
1117 // branch is a tree first: stepping into its cells is what Tab
1118 // and Enter are for, and a carousel elsewhere on the screen is
1119 // not what the caret is on.
1120 if let Some(Spot::Row {
1121 branch: Some(branch),
1122 open,
1123 ..
1124 }) = &here
1125 && *open != matches!(key, Key::Right)
1126 {
1127 let branch = branch.clone();
1128 self.view.fold(&branch, *open);
1129 // The rows under it arrived or went away, so the stops
1130 // moved under the caret. Held to what is reachable now, for
1131 // the reason `prune` clamps it.
1132 let stops = self.reaches().len();
1133 self.view
1134 .focus_on(self.view.focus().min(stops.saturating_sub(1)), stops);
1135 return Step::Idle;
1136 }
1137 // The table row under the caret next, when it has controls in
1138 // it. `27f2331e`: the row is one stop and these are the keys
1139 // that get inside it. Ahead of the region below because the
1140 // thing under the caret beats a search of the whole screen --
1141 // see this module's header for what that costs.
1142 if let Some(Spot::Row { inside, .. }) = &here
1143 && !inside.is_empty()
1144 {
1145 let steps = if matches!(key, Key::Right) { 1 } else { -1 };
1146 self.view.step_inside(steps, inside.len());
1147 return Step::Idle;
1148 }
1149 // The region the caret is in, when that region shows one child
1150 // at a time, and otherwise the first one on the screen that
1151 // does. The fallback is not a convenience: a carousel's frames
1152 // are pictures, so there is nothing reachable inside one and
1153 // focus can never be in it. Without this the one widget that
1154 // asked for these keys could not be reached by them.
1155 if let Some(slot) = self.moving(&reaches) {
1156 let steps = if matches!(key, Key::Right) { 1 } else { -1 };
1157 let slot = slot.clone();
1158 self.view.show_by(&slot, steps);
1159 // `dfbc88ce`. A tab's panel is a route, and the strip is
1160 // what calls it: the browser puts the address on the button
1161 // and htmx fires on the press, and this is the same press.
1162 // Only when the panel is empty, so pressing back to a tab
1163 // already read is free, which is what the reader means by
1164 // going back to it.
1165 if let Some(action) = self
1166 .view
1167 .shown(&slot)
1168 .and_then(|at| slot.body.get(at))
1169 .and_then(|placed| match &placed.node {
1170 Node::Region(panel) => panel.asked_for(),
1171 _ => None,
1172 })
1173 && let Some(request) = Self::request_for(action)
1174 {
1175 return Step::Call(request);
1176 }
1177 }
1178 Step::Idle
1179 }
1180
1181 // The innermost thing first, all the way out: Escape steps out of
1182 // a table row, then closes what is on top, then goes back. Which is
1183 // what Escape means everywhere else it is bound. The short circuit
1184 // is what makes it one at a time: a press that left a row does not
1185 // also dismiss the overlay the row is on.
1186 Key::Escape => {
1187 if self.view.leave() || self.dismiss() {
1188 Step::Idle
1189 } else {
1190 self.back()
1191 }
1192 }
1193
1194 Key::Backspace => {
1195 if let Some(field) = caret.as_ref().and_then(Spot::field) {
1196 self.view.backspace(field);
1197 // A delete asks nothing -- see `after_typing`, which a
1198 // delete does not reach -- and it can still take the
1199 // candidates away: they are about a value that has just
1200 // stopped earning them.
1201 self.unearned(field);
1202 }
1203 Step::Idle
1204 }
1205
1206 Key::Enter => match caret {
1207 Some(Spot::Act {
1208 action,
1209 confirm,
1210 over,
1211 asks,
1212 fills,
1213 copies,
1214 ..
1215 }) => {
1216 // A control over an empty selection is drawn disabled
1217 // (`commit_line`), and this is the other half of saying so:
1218 // pressing it does nothing rather than calling a route that
1219 // will act on nothing and answer "0 tasks completed". The
1220 // focus ring still stops on it, deliberately -- the reach
1221 // walk reads the description and the ticks are the view's,
1222 // so skipping it here would leave the two disagreeing about
1223 // how many stops a screen has.
1224 if over.is_some() && self.view.ticks().next().is_none() {
1225 return Step::Idle;
1226 }
1227 let mut payload = self.gathering(over.as_deref());
1228 payload.absorb(self.asked(&asks));
1229 self.deposit(fills.as_ref());
1230 // Before the call, matching `deposit` above and the other
1231 // two renderers: the local half of a press happens whether
1232 // or not anything is asked, and a copying act asks nothing.
1233 if let Some(value) = copies {
1234 return Step::Copy(value);
1235 }
1236 match confirm {
1237 Some(prompt) => {
1238 self.asked = Some((action, payload));
1239 Step::Ask(prompt)
1240 }
1241 None => self.send(&action, payload),
1242 }
1243 }
1244 Some(Spot::Submit { action, names }) => {
1245 let payload = self.view.submission(
1246 &names,
1247 // Every described box, including one in a region that
1248 // does not apply right now: a browser sends the value
1249 // of a hidden input, and one description submitted on
1250 // two hosts has to send the same form. `079a011e`.
1251 &self
1252 .described()
1253 .into_iter()
1254 .map(|reach| reach.spot)
1255 .collect::<Vec<_>>(),
1256 );
1257 self.send(&action, payload)
1258 }
1259 // The controls of a question answered N times. `60d1753c`:
1260 // pressing one changes how many boxes there are and asks
1261 // nothing of any route, which is the third of the three things
1262 // the member is for. The caret stays where it is, so a reader
1263 // adding three slots presses add three times without walking
1264 // back to it.
1265 Some(Spot::Repeat { field, at }) => {
1266 match at {
1267 Some(at) => self.view.remove_slot(&field, at),
1268 None => self.view.add_slot(&field),
1269 }
1270 // The stops moved under the caret: a slot arrived above the
1271 // add control, or one went away above it. Held to what is
1272 // reachable now, for the reason `prune` clamps it.
1273 let stops = self.reaches().len();
1274 self.view
1275 .focus_on(self.view.focus().min(stops.saturating_sub(1)), stops);
1276 Step::Idle
1277 }
1278 // A field takes Enter and does nothing with it. A browser
1279 // submits the form around it, and doing that here would fire a
1280 // write from the first box the user finished typing in; the
1281 // submit is one Tab away and says what it does.
1282 Some(Spot::Field(_)) | None => Step::Idle,
1283 // A row of a live selection says how the press was meant, which
1284 // for Enter is always the plain reading: this row and nothing
1285 // else. Space below is the other one a terminal has. `1894e95d`.
1286 Some(Spot::Row {
1287 activate: Some(action),
1288 chosen: Some(_),
1289 ..
1290 }) => {
1291 let action = action.clone();
1292 self.send(&action, meant(Choosing::Only))
1293 }
1294 Some(other) => match other.enters() {
1295 Some(action) => self.call(&action.clone()),
1296 None => Step::Idle,
1297 },
1298 },
1299
1300 // The row's own key, so it takes `here` and not `caret`: a tick
1301 // belongs to the row and there is nothing in a cell that could hold
1302 // one. A caret that has stepped into a row therefore ticks nothing,
1303 // and Escape is one press away from the row that does.
1304 Key::Char(' ') if !self.editing() => match here {
1305 // A tick is a write when the description says it is, and
1306 // staged selection when it does not. `toggle` first, because a
1307 // row carrying one has said the tick *is* the write and that
1308 // claim beats the screen's set.
1309 Some(Spot::Row {
1310 toggle: Some(action),
1311 ..
1312 }) => self.call(&action),
1313 // Otherwise it joins or leaves the set the screen names. The
1314 // hole `5f2b8753` was filed for was here: this used to be
1315 // `Step::Idle`, so the box was drawn, the key was bound, and
1316 // pressing it did nothing.
1317 //
1318 // Still idle when a row names no value or the screen holds no
1319 // set, which is the same description bug one step earlier. A
1320 // key bound to nothing is what this stopped doing, so it does
1321 // not start doing it again by accepting a tick that cannot be
1322 // read back.
1323 Some(Spot::Row {
1324 ticked: Some(_),
1325 value: Some(value),
1326 ..
1327 }) if self.screen.selection.is_some() => {
1328 self.view.tick(&value);
1329 Step::Idle
1330 }
1331 // The terminal's ctrl-click. A live selection is the app's, so
1332 // unlike a tick this does not write into the view: it calls the
1333 // row's own address and says the press meant "and also this
1334 // one", and the app answers with a screen where it is chosen.
1335 // `1894e95d`.
1336 //
1337 // After the tick arms, deliberately. A row carrying both has
1338 // said two contradictory things and the staged reading is the
1339 // older one; nothing in the tree does, and the order is what
1340 // decides it if anything ever does.
1341 Some(Spot::Row {
1342 activate: Some(action),
1343 chosen: Some(_),
1344 ..
1345 }) => self.send(&action, meant(Choosing::Also)),
1346 _ => Step::Idle,
1347 },
1348
1349 Key::Char(ch) => {
1350 if let Some(field) = caret.as_ref().and_then(Spot::field).cloned() {
1351 self.type_into(&field, ch);
1352 return self.after_typing(caret.as_ref());
1353 }
1354 // Not in a field, so the key is a shortcut if any control on
1355 // the screen claimed it. `Act::key` is text rather than a
1356 // modelled chord, so this is a string comparison against what
1357 // the description wrote, and a name this renderer does not
1358 // understand simply never matches.
1359 let pressed = ch.to_string();
1360 let claimed = reaches.iter().find_map(|reach| match &reach.spot {
1361 // A key reaching a commit control over an empty selection
1362 // is refused for the same reason a press on it is.
1363 Spot::Act {
1364 action,
1365 key: Some(key),
1366 over,
1367 asks,
1368 fills,
1369 copies,
1370 ..
1371 } if *key == pressed
1372 && (over.is_none() || self.view.ticks().next().is_some()) =>
1373 {
1374 Some((
1375 action.clone(),
1376 over.clone(),
1377 asks.clone(),
1378 fills.clone(),
1379 copies.clone(),
1380 ))
1381 }
1382 _ => None,
1383 });
1384 match claimed {
1385 Some((action, over, asks, fills, copies)) => {
1386 let mut payload = self.gathering(over.as_deref());
1387 payload.absorb(self.asked(&asks));
1388 self.deposit(fills.as_ref());
1389 // The bound key and Enter are one press said two ways,
1390 // so a copy reaches the host from either.
1391 if let Some(value) = copies {
1392 return Step::Copy(value);
1393 }
1394 self.send(&action, payload)
1395 }
1396 None => Step::Idle,
1397 }
1398 }
1399 }
1400 }
1401
1402 /// Put fresh contents wherever this names, and say whether anywhere did.
1403 ///
1404 /// The panel is not on the screen and is addressable all the same, so an
1405 /// answer aimed at it lands in the chrome. Asked in that order rather than
1406 /// the other way round because the panel's id is the app's and a screen
1407 /// could carry a region with the same name, and the app's panel is the one
1408 /// that outlives the screen.
1409 fn land(&mut self, region: &str, node: Node) -> bool {
1410 if self.chrome.panel(region).is_some() {
1411 return self.chrome.replace(region, node);
1412 }
1413 self.screen.replace(region, node)
1414 }
1415
1416 /// Put what the router answered onto the screen.
1417 ///
1418 /// Answers with a follow-up request when the response says to go somewhere
1419 /// else, which the host performs the same way it performed the first one.
1420 /// `request` is what was asked, because whether an answer is a place is
1421 /// derived from it: a read that answered a whole screen is somewhere you
1422 /// can come back to, and a write is not.
1423 pub fn apply(&mut self, request: &Request, response: Response) -> Option<Request> {
1424 let Response {
1425 outcome,
1426 notice,
1427 address,
1428 invalidates,
1429 } = response;
1430 self.saying = notice.or(self.saying.take());
1431
1432 // Whatever was outstanding has been answered. Only the request that went
1433 // out clears it: an answer to something else arriving first leaves the
1434 // control locked, which is what it means for that control to still be
1435 // waiting.
1436 if self
1437 .outstanding
1438 .as_ref()
1439 .is_some_and(|(_, sent)| sent == request)
1440 {
1441 self.outstanding = None;
1442 self.view.awaiting(None);
1443 }
1444
1445 match outcome {
1446 // Invalidations are not applied to a whole screen, matching what an
1447 // HTTP host does with them and for the same reason: every region is
1448 // being replaced already, so naming one of them again says nothing
1449 // the new screen does not.
1450 // Over what is already there. The layer underneath is put away
1451 // whole -- its screen and the view holding everything the user did
1452 // to it -- and comes back untouched when the overlay is dismissed.
1453 //
1454 // `remember` is deliberately not called: an overlay is not a place,
1455 // so history is left exactly as it was and Escape from the overlay
1456 // reveals rather than navigates.
1457 // The candidates for the box being typed into. Not a region and
1458 // not a screen: the list belongs to a control, so it lands on the
1459 // view beside what has been typed rather than in the description.
1460 Outcome::Suggestions { field, options } => {
1461 self.view.suggested(field, options);
1462 None
1463 }
1464 Outcome::Over(screen) => {
1465 self.layer(request, screen, Laid::Over);
1466 None
1467 }
1468 // Over what is already there, at something on it. The same layering,
1469 // plus one decision: where the box goes.
1470 //
1471 // The anchor is resolved against the screen being covered rather
1472 // than against the one arriving, because that is the screen it
1473 // names -- a menu is anchored to what was showing when it opened.
1474 // `Screen::anchors` is where the walk lives, so all three renderers
1475 // answer the question the same way.
1476 //
1477 // An anchor naming nothing on that screen falls back to the overlay
1478 // box. That is a description bug and gets a description bug's
1479 // treatment everywhere else in this crate: the screen still draws,
1480 // and the loss is the placement rather than the menu.
1481 Outcome::Anchored { screen, anchor } => {
1482 let laid = if self.screen.anchors(&anchor) {
1483 Laid::Anchored {
1484 low: self.subject_is_high(),
1485 }
1486 } else {
1487 Laid::Over
1488 };
1489 self.layer(request, screen, laid);
1490 None
1491 }
1492 // A whole screen replaces everything, including any overlay open
1493 // over it. A route that answers with a screen is a navigation, and
1494 // navigating with a palette still floating over the destination is
1495 // the state nobody asked for.
1496 Outcome::Screen(screen) => {
1497 // Arriving where you already are is a refresh rather than a
1498 // navigation, and the difference is the whole of what the user
1499 // has done to the screen. `reset` and `seed` below are both
1500 // *arrival* behaviour, so a refresh runs neither: otherwise a
1501 // reload clears the field being typed into and puts back an
1502 // untick the moment anything redraws.
1503 let refreshed = self.here.as_ref() == Some(request);
1504 self.under.clear();
1505 self.over = None;
1506 self.laid = None;
1507 self.remember(request, address.as_ref());
1508 self.screen = screen;
1509 self.reraise(std::time::Instant::now());
1510 if !refreshed {
1511 self.view.reset();
1512 // The rows a new screen says are already ticked. After this
1513 // the user's ticks are the truth, which is why it is applied
1514 // once on arrival rather than read on every draw.
1515 self.view.seed(&self.screen);
1516 // And where the caret starts, for the same reason and at the
1517 // same moment: a refresh runs neither, or a reload would
1518 // snatch the caret back out of whatever is being typed into.
1519 self.open_caret();
1520 }
1521 self.announce();
1522 None
1523 }
1524 Outcome::Fragment { region, node } => {
1525 // A region that is not there is the description bug
1526 // `Screen::replace` describes, and a terminal can say so
1527 // rather than swallowing it: the region it named is gone, and
1528 // drawing nothing would look like a control that does nothing.
1529 //
1530 // The slots the answer invalidated go in the same way. On a
1531 // terminal that is the whole of what invalidation means: the
1532 // next frame redraws everything, so putting the new contents
1533 // on the screen is putting them in front of the user. What a
1534 // webview needs an out-of-band swap for, this gets for free.
1535 let mut missing: Vec<String> = Vec::new();
1536 if !self.land(&region, node) {
1537 missing.push(region);
1538 }
1539 for stale in invalidates {
1540 if !self.land(&stale.region, stale.node) {
1541 missing.push(stale.region);
1542 }
1543 }
1544 if !missing.is_empty() {
1545 // One message naming all of them, rather than a banner per
1546 // region where only the last would survive.
1547 let named = missing
1548 .iter()
1549 .map(|region| format!("`{region}`"))
1550 .collect::<Vec<_>>()
1551 .join(", ");
1552 let subject = if missing.len() == 1 { "is" } else { "are" };
1553 self.saying = Some(Message {
1554 kind: layout::Notice::Banner,
1555 tone: layout::Tone::Danger,
1556 text: format!("nothing on this screen {subject} called {named}"),
1557 undo: None,
1558 });
1559 }
1560 self.view.prune(&self.screen, &self.frame, &self.chrome);
1561 self.announce();
1562 None
1563 }
1564 // The work was handed off and the region is waiting on it. A
1565 // retained-screen host needs no markup for this: the slot's
1566 // readiness is the axis, `region::draw` already writes "Loading"
1567 // from it, and the next thing the region is told puts content back.
1568 //
1569 // A region that is not there is the same description bug a fragment
1570 // naming one is, and gets the same banner. Said once here rather
1571 // than folded into the arm above: that one collects misses across
1572 // the answer and its invalidations, and there is only ever one
1573 // region here.
1574 //
1575 // The message goes in as a stand-in and this renderer will not draw
1576 // it, because a pending region returns before it reaches the body.
1577 // Applied anyway rather than dropped: what is on the screen is the
1578 // description, and a host that later draws a pending body — or a
1579 // test asking what the region holds — should find what the route
1580 // said rather than what was there before.
1581 Outcome::Started { region, message } => {
1582 if !self.screen.started(&region, message) {
1583 self.saying = Some(Message {
1584 kind: layout::Notice::Banner,
1585 tone: layout::Tone::Danger,
1586 text: format!("nothing on this screen is called `{region}`"),
1587 undo: None,
1588 });
1589 }
1590 self.view.prune(&self.screen, &self.frame, &self.chrome);
1591 self.announce();
1592 None
1593 }
1594 Outcome::Goto(action) => match self.call(&action) {
1595 Step::Call(request) => Some(request),
1596 // An external destination is the host's to open, and there is
1597 // nothing to come back for.
1598 _ => None,
1599 },
1600 // A file for the host to put somewhere. Not a screen, not a
1601 // region, and not a place: what is showing stays showing, and the
1602 // answer leaves by `handed` rather than by the return value.
1603 Outcome::File { name, kind, bytes } => {
1604 self.handed = Some(Handed {
1605 name: safe_file_name(&name),
1606 kind,
1607 bytes,
1608 });
1609 None
1610 }
1611 // A place for the host to go and find. Not a screen, not a region
1612 // and not a place in the other sense either: what is showing stays
1613 // showing, and the ask leaves by `locating`.
1614 Outcome::Locate(asking) => {
1615 self.locating = Some(asking);
1616 None
1617 }
1618 }
1619 }
1620
1621 /// Take the file the last answer handed over, if it handed one over.
1622 ///
1623 /// Called after [`apply`](Self::apply), the way
1624 /// [`refreshes`](Self::refreshes) is called after a draw: this drains, so a
1625 /// host that calls it twice gets the file once. A host that never calls it
1626 /// silently drops every download, which is why the file arrives here rather
1627 /// than being written where the runtime cannot be tested.
1628 ///
1629 /// What to do with it is the ruling's: write it into the working directory
1630 /// under [`Handed::name`]. See [`Handed`].
1631 pub fn handed(&mut self) -> Option<Handed> {
1632 self.handed.take()
1633 }
1634
1635 /// Take the place the last answer asked for, if it asked for one.
1636 ///
1637 /// [`handed`](Self::handed)'s twin: drained after
1638 /// [`apply`](Self::apply), once per ask, and dropped silently by a host
1639 /// that never calls it. Build the follow-up call with
1640 /// [`Locating::answered`] once the place is known and hand the answer back
1641 /// to [`apply`](Self::apply); a reader who backs out is not an answer and
1642 /// nothing goes to the router.
1643 ///
1644 /// # A terminal has no picker, and that is not a gap
1645 ///
1646 /// The other renderers open a native dialog. A terminal has none, and the
1647 /// honest reading is the one [`Handed`] already takes on this host: the
1648 /// place is the working directory unless the app has somewhere better, and
1649 /// a host that wants to ask can read [`Locating::prompt`] and ask however
1650 /// it asks — a prompt line, a picker of its own, an argument it was started
1651 /// with. What it must not do is guess and stay quiet, because the handle it
1652 /// answers with is where a long export is about to write hundreds of files.
1653 ///
1654 /// # The save shape, on a host with no save dialog
1655 ///
1656 /// [`Sought::Save`](quasi_router::Sought::Save) carries the name the
1657 /// dialog would have opened with, which is exactly what a terminal needs
1658 /// to answer without one: the working directory joined to
1659 /// [`safe_file_name`] of that name, said out loud so the reader can see
1660 /// where it went. The suggestion is frequently built from something they
1661 /// typed earlier, so it goes through the sanitiser here for the reason it
1662 /// does on a download: this host writes beside the process, and
1663 /// `../../.ssh/authorized_keys` is a name a description can hold. A host
1664 /// that would rather ask reads the name as the default in its own prompt.
1665 ///
1666 /// # One call, whatever was picked
1667 ///
1668 /// [`Locating::answered`] takes every
1669 /// [`Picked`](quasi_router::Picked) at once and builds one request, so an
1670 /// ask for several files is answered once with all of them. A host that
1671 /// calls it per file makes N requests out of one ask and turns a batched
1672 /// import into N imports.
1673 pub fn locating(&mut self) -> Option<Locating> {
1674 self.locating.take()
1675 }
1676
1677 /// Go back, if there is anywhere to go.
1678 /// A key as the text a [`Chrome`] binding names it by.
1679 ///
1680 /// The same string comparison `Act::key` gets, for the same reason: the
1681 /// vocabulary of keys is the host's, and this host's names are these. A
1682 /// modifier this renderer cannot receive is a name that never matches,
1683 /// which is what a binding for another host should do here.
1684 fn key_name(key: Key) -> Option<String> {
1685 Some(match key {
1686 Key::Char(ch) => ch.to_string(),
1687 Key::Enter => "enter".into(),
1688 Key::Escape => "escape".into(),
1689 Key::Tab => "tab".into(),
1690 Key::BackTab => "backtab".into(),
1691 Key::Up => "up".into(),
1692 Key::Down => "down".into(),
1693 Key::PageUp => "pageup".into(),
1694 Key::PageDown => "pagedown".into(),
1695 Key::Backspace => "backspace".into(),
1696 Key::Left => "left".into(),
1697 Key::Right => "right".into(),
1698 })
1699 }
1700
1701 /// The region the arrow keys move, if the screen has one.
1702 ///
1703 /// The one the caret is in when that region shows one child at a time, and
1704 /// otherwise the first such region in draw order. Two rules rather than one
1705 /// because focus is not always a usable answer here: a carousel holds
1706 /// pictures, nothing in it is reachable, and a rule that only ever asked
1707 /// where the caret was would leave the widget that wanted these keys unable
1708 /// to be reached by them.
1709 ///
1710 /// A screen with two of these and no focus in either moves the first, which
1711 /// is arbitrary and is said out loud rather than hidden. Nothing in the tree
1712 /// has two yet; the screen that does is the one that will want a reachable
1713 /// control instead, and that is a `Spot` rather than a rule here.
1714 fn moving<'a>(&'a self, reaches: &[crate::focus::Reach]) -> Option<&'a Slot> {
1715 let here = reaches
1716 .get(self.view.focus())
1717 .and_then(|reach| self.find(&reach.region))
1718 .filter(|slot| slot.showing().selective());
1719 here.or_else(|| self.screen.slots.iter().find_map(Self::selective))
1720 }
1721
1722 /// This slot or the first under it that shows one child at a time.
1723 fn selective(slot: &Slot) -> Option<&Slot> {
1724 if slot.showing().selective() {
1725 return Some(slot);
1726 }
1727 slot.body.iter().find_map(|placed| match &placed.node {
1728 Node::Region(inner) => Self::selective(inner),
1729 _ => None,
1730 })
1731 }
1732
1733 /// The slot under this address, anywhere on the screen.
1734 fn find(&self, region: &str) -> Option<&Slot> {
1735 self.screen.slots.iter().find_map(|slot| slot.find(region))
1736 }
1737
1738 /// Close the overlay on top, if there is one.
1739 ///
1740 /// The layer underneath comes back exactly as it was left: its own focus,
1741 /// its own edits, its own scroll. That is the whole reason a layer carries
1742 /// its own [`View`], and history is not touched because an overlay was
1743 /// never a place.
1744 fn dismiss(&mut self) -> bool {
1745 match self.under.pop() {
1746 Some(layer) => {
1747 self.screen = layer.screen;
1748 self.view = layer.view;
1749 // The outer overlay's identity, or `None` back on the base
1750 // screen. Restored rather than cleared, so dismissing a confirm
1751 // raised over a palette leaves the palette still refusing to
1752 // stack itself.
1753 self.over = layer.over;
1754 self.laid = layer.laid;
1755 self.reraise(std::time::Instant::now());
1756 true
1757 }
1758 None => false,
1759 }
1760 }
1761
1762 /// Go back, if there is anywhere to go.
1763 ///
1764 /// What [`Destination::Back`](quasi_router::Destination::Back) is answered
1765 /// with, and what this runtime's own Escape reaches once there is no
1766 /// overlay left to dismiss. `Step::Idle` at the bottom of the stack: the
1767 /// first screen is not somewhere you arrived at.
1768 fn back(&mut self) -> Step {
1769 match self.history.pop() {
1770 Some(request) => {
1771 self.here = Some(request.clone());
1772 Step::Call(request)
1773 }
1774 None => Step::Idle,
1775 }
1776 }
1777
1778 /// Note where we were, before we leave it.
1779 ///
1780 /// The derivation the response's own documentation describes: a read that
1781 /// answered a screen is a place, everything else is not, and
1782 /// [`Address`](quasi_router::Address) is the override for the two cases the
1783 /// derivation cannot reach.
1784 ///
1785 /// Arriving where you already are is not leaving anywhere, which is what the
1786 /// first guard is for: [`reload`](Self::reload) answers the request that
1787 /// produced the screen showing, so without it every refresh would push a
1788 /// duplicate of the current place and `back` would walk through a stack of
1789 /// the screen it is already on.
1790 fn remember(&mut self, request: &Request, address: Option<&quasi_router::Address>) {
1791 if self.here.as_ref() == Some(request) {
1792 return;
1793 }
1794 let place = match address {
1795 Some(quasi_router::Address::Enters(_)) => true,
1796 Some(quasi_router::Address::Unchanged) => false,
1797 Some(quasi_router::Address::Replaces(_)) => {
1798 self.here = Some(request.clone());
1799 return;
1800 }
1801 None => request.method == Method::Get,
1802 };
1803 if place && let Some(previous) = self.here.replace(request.clone()) {
1804 self.history.push(previous);
1805 }
1806 }
1807
1808 /// Put whatever the response wanted said onto the screen it belongs to.
1809 fn announce(&mut self) {
1810 let Some(message) = self.saying.take() else {
1811 return;
1812 };
1813 // The way back the response offered, as the control it becomes here.
1814 // `bde35298`: this used to be dropped, because `Node::Notice` had
1815 // nowhere to hang a control and the comment saying so was the whole of
1816 // the answer. The member exists now.
1817 let act = message.undo_act();
1818 let Message {
1819 kind, tone, text, ..
1820 } = message;
1821 if kind.transient() {
1822 self.raised.push(std::time::Instant::now());
1823 }
1824 self.screen.notices.push(Node::Notice {
1825 kind,
1826 tone,
1827 text,
1828 act,
1829 });
1830 }
1831
1832 /// Type into a field, honouring what the description says it will take.
1833 fn type_into(&mut self, field: &crate::FieldSpot, ch: char) {
1834 if matches!(field.kind, layout::FieldKind::Checkbox) {
1835 // A checkbox holds one of two values, so a key does not type into
1836 // it: any key flips it, which is what space does to one in a
1837 // browser and is the only sentence a box with two states can hear.
1838 let ticked = self.view.typed(field) == Node::SELECTED;
1839 let next = if ticked {
1840 String::new()
1841 } else {
1842 Node::SELECTED.to_string()
1843 };
1844 self.view.set(&field.name, next);
1845 return;
1846 }
1847
1848 // `Field::max_length` is a rule the description carries and every
1849 // renderer emits in its host's idiom. A browser stops accepting
1850 // characters, and so does this.
1851 if let Some(limit) = field.max_length
1852 && self.view.typed(field).chars().count() >= limit as usize
1853 {
1854 return;
1855 }
1856 self.view.push(field, ch);
1857 }
1858
1859 /// What a keystroke in a field costs: a write when the field writes as it
1860 /// changes, an ask when it consults something about what is being typed.
1861 ///
1862 /// A write beats a consult when a field carries both, because the write is
1863 /// what the keystroke *means* and the ask is about it. One keystroke
1864 /// produces one [`Step`], so the consult goes unasked in that case; no
1865 /// measured site carries both, and inventing a queue for a shape nothing
1866 /// has asked for is how this crate would grow a scheduler.
1867 ///
1868 /// The regions holding the box ask under the same rule and lose it the same
1869 /// way, which is worth stating because it is the less obvious half: a dial
1870 /// that writes on its own inside a panel that recomputes writes, and the
1871 /// panel is left to the next keystroke. The write cannot join the delayed
1872 /// set instead -- [`Step::CallAfter`] says a later one replaces an
1873 /// outstanding one whole, so a write parked in there is a write a keystroke
1874 /// can cancel.
1875 /// The write a field owes when the caret leaves it, if it owes one.
1876 ///
1877 /// `Field::writes` means the change is complete, and on a terminal a value
1878 /// built up character by character is complete when the reader walks off
1879 /// it. The same moment a browser raises `change`, which is what `quasi-
1880 /// webview` emits and what the pick site above already calls "what a
1881 /// settled value costs".
1882 ///
1883 /// `None` when there is nothing to say: not a field, no `changes`, or a
1884 /// value nobody altered. Walking through a form must not write every box it
1885 /// passes.
1886 fn leaving(&mut self, here: Option<&Spot>) -> Option<Step> {
1887 let field = here.and_then(Spot::field)?;
1888 if field.kind.offers_options() || field.kind.takes_files() || field.kind.offers_themes() {
1889 // Already written when it changed, for the reason `after_typing`
1890 // gives: one interaction, one whole answer.
1891 return None;
1892 }
1893 let action = field.writes.clone()?;
1894 let name = field.name.clone();
1895 // Seeded from the description when the box is untouched, which is what
1896 // is showing in it. `unwrap_or_default` here read an untouched box as
1897 // empty, so walking through a form wrote every prefilled field it
1898 // passed -- caught by `walking_through_a_box_nobody_altered_writes_nothing`.
1899 let described = field.value.as_deref().unwrap_or_default();
1900 let value = self.view.edit(&name).unwrap_or(described).to_string();
1901 if !self.view.unwritten(&name, &value, field.value.as_deref()) {
1902 return None;
1903 }
1904 self.view.wrote(&name, &value);
1905 let mut payload = Params::new();
1906 payload.insert(name, value);
1907 Some(self.send(&action, payload))
1908 }
1909
1910 fn after_typing(&mut self, here: Option<&Spot>) -> Step {
1911 let Some(field) = here.and_then(Spot::field) else {
1912 return Step::Idle;
1913 };
1914 let name = field.name.clone();
1915
1916 // `8032fe61`. A keystroke is not a write. `Field::writes` means the
1917 // change is complete, which is what `quasi-webview` has always emitted
1918 // it as -- `hx-trigger="change"`, raised on blur or Enter for a text
1919 // control -- and this fired on every character, so a search box was one
1920 // request per letter and a bounded number posted every value on the way
1921 // to the one being typed.
1922 //
1923 // What is left of the old shape is exactly the controls a browser fires
1924 // `change` for at once: a value chosen in one go was complete when it
1925 // changed. Everything the reader builds up waits for `leaving`.
1926 // A theme is picked in one gesture, so it belongs with the options
1927 // and not with the values a reader builds up character by character.
1928 let built_up = !(field.kind.offers_options()
1929 || field.kind.takes_files()
1930 || field.kind.offers_themes());
1931 if let Some(action) = field.writes.clone().filter(|_| !built_up) {
1932 let value = self.view.edit(&name).unwrap_or_default().to_string();
1933 let mut payload = Params::new();
1934 payload.insert(name, value);
1935 return self.send(&action, payload);
1936 }
1937
1938 // Asking about the value carries the value and nothing else. No ticks:
1939 // a consult is a question about this box, not a control over a set.
1940 // The question the field owns is asked alongside the ones it merely
1941 // asks, and it is asked the same way: the answer differs, not the
1942 // asking. Dropping under its floor closes the list rather than leaving
1943 // candidates for a value that no longer earns them.
1944 let questions: Vec<&Consult> = field.suggests.iter().chain(field.consults.iter()).collect();
1945 self.unearned(field);
1946
1947 let value = self.view.edit(&name).unwrap_or_default().to_string();
1948 let mut asks = Vec::new();
1949 for consult in questions {
1950 // Too little to ask about yet, and asked per question rather than
1951 // for the field: the two questions about one box carry their own
1952 // floors, and MNW's do differ from each other in wait even where
1953 // the floors agree.
1954 if !consult.asks_about(&value) {
1955 continue;
1956 }
1957 // What else rides along, which is the half that made discover's
1958 // results route unsayable: it answers about the current filters,
1959 // and a question asked without them answers about a screen the
1960 // user is not looking at.
1961 let mut payload = self.contributed(&consult.sends);
1962 payload.insert(name.clone(), value.clone());
1963 match self.send(&consult.action, payload) {
1964 Step::Call(request) => asks.push(Delayed {
1965 request,
1966 after: consult.after,
1967 }),
1968 // A consult that names somewhere outside the app is not a
1969 // question this runtime can wait on. `Self::send` already
1970 // answers for it, and delaying it would be a delay before
1971 // opening a browser.
1972 other => return other,
1973 }
1974 }
1975
1976 // The regions this box sits in, each asking about the whole set of
1977 // dials it contains rather than about this one value. `cb62a9dc`: on a
1978 // browser the question is one element's and the values are gathered by
1979 // containment; here the containment is walked instead, and the two
1980 // hosts gather the same set because `Slot::questions` is the only walk
1981 // either of them reads.
1982 match self.region_asks(&name, &value) {
1983 Ok(region) => asks.extend(region),
1984 Err(other) => return other,
1985 }
1986
1987 // `Idle` and not an empty delayed call: nothing is outstanding here
1988 // — the wait lives with the host, which is what keeps a clock out
1989 // of this crate — so a keystroke under every floor costs nothing
1990 // and deleting back under them asks nothing.
1991 if asks.is_empty() {
1992 return Step::Idle;
1993 }
1994 Step::CallAfter { asks }
1995 }
1996
1997 /// What the regions holding this box ask when it moves.
1998 ///
1999 /// [`Slot::consults`](quasi_router::Slot::consults). Every region on the
2000 /// screen that both carries a question of its own and contains this field,
2001 /// at any depth, which is what makes a panel nested three groups deep
2002 /// recompute from a dial in one of them.
2003 ///
2004 /// The floor is read against the value that moved, never against the
2005 /// gathered set, which is [`Consult::asks_about`]'s own rule and the same
2006 /// thing the browser's `event.target.value` filter says.
2007 ///
2008 /// `Err` carries a step that is not a question this runtime can wait on --
2009 /// an address outside the app -- exactly as the field's own consults hand
2010 /// one back.
2011 fn region_asks(&mut self, moved: &str, value: &str) -> Result<Vec<Delayed>, Step> {
2012 // Read whole before anything is sent, because sending takes the
2013 // runtime mutably and the questions are read out of the screen it
2014 // holds. What is kept is the two small things a question needs -- the
2015 // consult and the values -- rather than the regions themselves, so a
2016 // keystroke does not clone a subtree.
2017 let mut asking: Vec<(Consult, Params)> = Vec::new();
2018 for slot in self.screen.consulting() {
2019 let inside = slot.questions();
2020 if !inside.iter().any(|field| field.name == moved) {
2021 continue;
2022 }
2023 // The dials, read the way a submit reads a form: what the reader
2024 // typed, falling back to what the description offered, so an
2025 // untouched box still sends what it is showing.
2026 let mut held = Params::new();
2027 for field in inside {
2028 let value = self
2029 .view
2030 .edit(&field.name)
2031 .map(str::to_owned)
2032 .or_else(|| field.value.clone());
2033 if let Some(value) = value {
2034 held = held.with(field.name.clone(), value);
2035 }
2036 }
2037 for consult in &slot.consults {
2038 if !consult.asks_about(value) {
2039 continue;
2040 }
2041 asking.push((consult.clone(), held.clone()));
2042 }
2043 }
2044
2045 let mut asks = Vec::new();
2046 for (consult, held) in asking {
2047 // What rides along from outside the region goes in first, so a dial
2048 // inside it wins where a name sits on both sides: the region's own
2049 // contents are what the question is about.
2050 let mut payload = self.contributed(&consult.sends);
2051 for (name, value) in held.iter() {
2052 payload = payload.with(name.to_owned(), value.to_owned());
2053 }
2054 match self.send(&consult.action, payload) {
2055 Step::Call(request) => asks.push(Delayed {
2056 request,
2057 after: consult.after,
2058 }),
2059 other => return Err(other),
2060 }
2061 }
2062 Ok(asks)
2063 }
2064
2065 /// Close an open list whose value no longer clears the floor.
2066 ///
2067 /// The floor is the description's answer to "is there enough here to be
2068 /// worth asking about", and candidates left over a value that no longer
2069 /// clears it are an answer to a question the field would not ask now.
2070 fn unearned(&mut self, field: &FieldSpot) {
2071 let Some(owned) = field.suggests.as_ref() else {
2072 return;
2073 };
2074 let value = self.view.edit(&field.name).unwrap_or_default().to_string();
2075 if !owned.asks_about(&value) {
2076 self.view.unsuggest();
2077 }
2078 }
2079
2080 /// The values other controls contribute to a question, by name.
2081 ///
2082 /// [`Consult::sends`](quasi_router::Consult::sends). What the user has
2083 /// typed, falling back to what the description offered, which is the same
2084 /// order a submit reads them in — an untouched filter still sends what it
2085 /// is showing.
2086 ///
2087 /// A name nothing on the screen carries contributes nothing rather than an
2088 /// empty value, so a route can tell "not on this screen" from "on it and
2089 /// blank". That is the miss `Consult::sends` documents, seen from the one
2090 /// host where it is cheap to notice.
2091 fn contributed(&self, names: &[String]) -> Params {
2092 let mut params = Params::new();
2093 if names.is_empty() {
2094 return params;
2095 }
2096 let spots = self.described();
2097 for name in names {
2098 let described = spots.iter().find_map(|reach| match &reach.spot {
2099 Spot::Field(field) if &field.name == name => Some(field),
2100 _ => None,
2101 });
2102 let Some(field) = described else { continue };
2103 let value = self
2104 .view
2105 .edit(name)
2106 .map(str::to_owned)
2107 .or_else(|| field.value.clone());
2108 if let Some(value) = value {
2109 params = params.with(name.clone(), value);
2110 }
2111 }
2112 params
2113 }
2114
2115 /// The ticks a control acting over a selection sends with its call.
2116 ///
2117 /// Empty for a control that names no selection. A control that names one
2118 /// sends the whole set, whatever it called it: see [`Act::over`] for why
2119 /// the name is not matched against the screen's, which is that a webview
2120 /// rendering a fragment has no screen to match it against and the two
2121 /// hosts would then disagree about a typo.
2122 ///
2123 /// Empty is also what a control over a set nobody ticked sends, and the two
2124 /// are deliberately the same. A handler receives a bulk action over
2125 /// nothing, which is a case it has to handle regardless.
2126 ///
2127 /// [`Act::over`]: quasi_router::Act::over
2128 fn gathering(&self, over: Option<&str>) -> Params {
2129 let mut payload = Params::new();
2130 if over.is_some() {
2131 for value in self.view.ticks() {
2132 payload.insert(Node::TICKED.to_owned(), value.to_owned());
2133 }
2134 }
2135 payload
2136 }
2137
2138 /// Put the act's value into the box it named.
2139 ///
2140 /// [`Act::fills`] names a field on this screen and the renderer decides
2141 /// where in it the value lands; here that is the end of what is already
2142 /// there. A terminal has no caret inside a field to insert at — `d52884b0`
2143 /// is why the view holds none — and the vocabulary calls appending correct
2144 /// rather than a fallback, because a description names a destination and
2145 /// never a position.
2146 ///
2147 /// After the payload is gathered, deliberately. A webview's htmx listener
2148 /// sits on the control and the fill script on the document, so there the
2149 /// press sends what the boxes held before it; doing it in the other order
2150 /// here would make one description send two different things on two hosts.
2151 ///
2152 /// From what the box is showing rather than from nothing: what has been
2153 /// typed if anything has, and otherwise what the description offered. A
2154 /// deposit that started from an empty string would silently discard the
2155 /// draft it was meant to add to, which is the exact defect the member was
2156 /// filed to stop the server-side workaround causing.
2157 ///
2158 /// A name no field on the screen carries writes an edit nothing reads,
2159 /// which is what a webview does with the same description: nothing is
2160 /// deposited and the act's own action still runs. A description naming a
2161 /// box that is not there is a description bug rather than a renderer's.
2162 ///
2163 /// [`Act::fills`]: quasi_router::Act::fills
2164 fn deposit(&mut self, fill: Option<&quasi_router::Prefill>) {
2165 let Some(fill) = fill else {
2166 return;
2167 };
2168 let described = self.described();
2169 let offered = described.iter().find_map(|reach| match &reach.spot {
2170 Spot::Field(field) if field.name == fill.field => field.value.as_deref(),
2171 _ => None,
2172 });
2173 let mut value = self.view.showing(&fill.field, offered).to_owned();
2174 value.push_str(&fill.value);
2175 self.view.set(&fill.field, value);
2176 }
2177
2178 /// The values a control asked for before it fired, by name.
2179 ///
2180 /// [`Act::asks`]. Read exactly as a submit reads a form's boxes, through
2181 /// the same view: an untouched box sends what the description offered, and
2182 /// an unticked checkbox sends nothing. A control that asked for nothing
2183 /// sends nothing, which is nearly all of them.
2184 ///
2185 /// [`Act::asks`]: quasi_router::Act::asks
2186 fn asked(&self, names: &[String]) -> Params {
2187 if names.is_empty() {
2188 return Params::new();
2189 }
2190 let spots: Vec<Spot> = self
2191 .described()
2192 .into_iter()
2193 .map(|reach| reach.spot)
2194 .collect();
2195 self.view.submission(names, &spots)
2196 }
2197
2198 /// An action as something the host can ask.
2199 fn call(&mut self, action: &Action) -> Step {
2200 self.send(action, Params::new())
2201 }
2202
2203 /// The request an action makes, or nothing when it names somewhere outside
2204 /// the app.
2205 ///
2206 /// Carries the action's own payload and the view it was offered under. What
2207 /// a control gathers on top of that is `send`'s, because only a press
2208 /// happens next to a selection.
2209 fn request_for(action: &Action) -> Option<Request> {
2210 let path = action.destination.route()?;
2211 Some(Request {
2212 method: action.method,
2213 path: path.to_string(),
2214 captures: Params::new(),
2215 payload: action.params.clone(),
2216 carried: action.carried.clone(),
2217 })
2218 }
2219
2220 /// An action, plus values the control is sending that are not on it.
2221 fn send(&mut self, action: &Action, extra: Params) -> Step {
2222 // Nothing to ask and nowhere to send anyone. This renderer is
2223 // `Renderer::Client`: it holds what it draws and redraws it from
2224 // memory, so whatever the local action names is something it already
2225 // does natively, and the mark tells it nothing it did not know.
2226 //
2227 // Handled rather than left to the guard below, which reads `route()` as
2228 // "not a route, therefore somewhere outside" and would hand the host an
2229 // empty address to open. `210574ca`.
2230 if action.destination.is_local() {
2231 return Step::Idle;
2232 }
2233 // Wherever the reader came from, which is this runtime's history and
2234 // not anything the description could have named. `33c27e81`. Before the
2235 // guard below for `Local`'s reason: `route()` is `None` here too, and
2236 // the guard would read that as "outside the app" and hand the host an
2237 // empty address to open.
2238 //
2239 // `back` dismisses nothing. An overlay is not a place -- `dismiss` says
2240 // so and keeps its own stack -- so a host that wants one key to do both
2241 // tries `dismiss` first, which is what this runtime's own Escape does.
2242 if action.destination.is_back() {
2243 return self.back();
2244 }
2245 if action.destination.route().is_none() {
2246 return Step::Open(action.destination.as_str().to_string());
2247 }
2248 // `a81384d4`. The description says this call is the host's, and this
2249 // host does not know the sequence behind it -- the case it was ruled on
2250 // is an upload, three requests to a browser and no file picker here to
2251 // start one. Calling the address anyway would ask a signing endpoint for
2252 // a screen and swap the JSON it answered with.
2253 //
2254 // Said rather than swallowed, the same as a fragment naming a region
2255 // that is not there: a control that is drawn, is reachable, and does
2256 // nothing when pressed is the hardest kind of bug to see from what is on
2257 // the screen.
2258 if action.by_host {
2259 self.saying = Some(Message {
2260 kind: layout::Notice::Banner,
2261 tone: layout::Tone::Danger,
2262 text: "this app cannot do that here".to_string(),
2263 undo: None,
2264 });
2265 // Announced here rather than left for the next navigation, because
2266 // no navigation is coming: this key produced no request, so nothing
2267 // downstream would ever move the message onto the screen.
2268 self.announce();
2269 return Step::Idle;
2270 }
2271 // The whole screen is being replaced, so this is an arrival rather than
2272 // a swap: whatever is open over the screen is put away before the call
2273 // leaves, and what comes back stands where the screen stood. That is
2274 // what "pushes a screen" means on a terminal, and it is the same
2275 // sentence the webview says by emitting the anchor and no verb.
2276 // `00ee7af5`, ruled 2026-08-25.
2277 //
2278 // Before the request is built, and not on the answer: `Outcome::Screen`
2279 // clears the overlay stack already, and a navigating call that answers
2280 // with anything else would otherwise land under a palette still
2281 // floating over the place it left.
2282 if action.navigates {
2283 self.under.clear();
2284 self.over = None;
2285 self.laid = None;
2286 }
2287 // `Action::elsewhere` gets no branch here, and that is the answer
2288 // rather than the gap. A mount of its own in a terminal would be a
2289 // split or a tab, which is this renderer's furniture and not the
2290 // description's: a screen asking for one would be asking for a layout.
2291 // So the call falls through and is performed where it stands, which is
2292 // the degradation the member's docs promise. goingson `3fb2526a`.
2293 let mut payload = extra;
2294 payload.absorb(action.params.clone());
2295 let Some(request) = Self::request_for(action).map(|request| Request { payload, ..request })
2296 else {
2297 return Step::Idle;
2298 };
2299
2300 // An awaiting call locks the control that made it until the answer
2301 // arrives. A terminal has no browser to do this for it, and the second
2302 // press is the one that buys the same thing twice.
2303 //
2304 // The refusal is against the outstanding request rather than against
2305 // being busy at all: the rest of the screen keeps working, which is what
2306 // "this control is doing something" means as opposed to "the app is".
2307 if action.awaits() {
2308 if self
2309 .outstanding
2310 .as_ref()
2311 .is_some_and(|(_, sent)| *sent == request)
2312 {
2313 return Step::Idle;
2314 }
2315 self.outstanding = Some((action.clone(), request.clone()));
2316 self.view.awaiting(Some(action.clone()));
2317 }
2318 Step::Call(request)
2319 }
2320 }
2321