Skip to main content

max / quasi

31.9 KB · 798 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 | back, or dismiss the question |
42
43 use makeover_layout as layout;
44 use quasi_router::{
45 Action, Chrome, Message, Method, Node, Outcome, Params, Request, Response, Screen, Slot,
46 };
47 use ratatui::buffer::Buffer;
48 use ratatui::layout::Rect;
49
50 use crate::focus::{Reach, Spot};
51 use crate::{Tui, View};
52
53 /// A key, named the way this crate wants to talk about one.
54 ///
55 /// Not crossterm's, deliberately. A host maps its own events onto this in a
56 /// dozen lines, and in exchange the bindings below are testable without a
57 /// terminal and this crate does not make every consumer take a backend it might
58 /// not be using.
59 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
60 pub enum Key {
61 /// A character the user typed.
62 Char(char),
63 /// Confirm, follow, submit.
64 Enter,
65 /// Forward through the reachable things.
66 Tab,
67 /// Backward through them.
68 BackTab,
69 /// Take a character back.
70 Backspace,
71 /// Out, back, never mind.
72 Escape,
73 /// Up one reachable thing.
74 Up,
75 /// Down one reachable thing.
76 Down,
77 /// A screen's worth backwards.
78 PageUp,
79 /// A screen's worth forwards.
80 PageDown,
81 /// Back one child, in a region showing one at a time.
82 Left,
83 /// On one child, in a region showing one at a time.
84 Right,
85 }
86
87 /// What the host should do about a key.
88 #[derive(Debug, Clone, PartialEq, Eq)]
89 pub enum Step {
90 /// Nothing left to do but redraw.
91 Idle,
92 /// Ask the router this, then hand the answer to [`Runtime::apply`].
93 Call(Request),
94 /// Ask this question. The next key answers it: `y` or Enter does the thing,
95 /// anything else does not.
96 Ask(String),
97 /// Somewhere outside the app. The host opens it, and nothing comes back.
98 Open(String),
99 }
100
101 /// A screen and what the user has done to it.
102 ///
103 /// The pair is the unit an overlay needs: an overlay has its own reach, its own
104 /// focus, its own edits and its own scroll, so it holds a [`View`] of its own
105 /// rather than borrowing the one underneath. Sharing it is the bug the overlay
106 /// tests exist to catch — dismissing a palette would take the user's typing and
107 /// scroll position with it.
108 #[derive(Debug, Clone)]
109 struct Layer {
110 screen: Screen,
111 view: View,
112 }
113
114 /// A screen, what the user has done to it, and how they got here.
115 #[derive(Debug, Clone)]
116 pub struct Runtime {
117 screen: Screen,
118 view: View,
119 /// What the app offers from every screen. Matched before this runtime's own
120 /// key table, so a screen cannot capture the key that opens the palette.
121 chrome: Chrome,
122 /// The layers this one is drawn over, outermost first.
123 ///
124 /// Empty on an ordinary screen. `screen` and `view` above are always the
125 /// ACTIVE layer, so every key, every gather and every draw works on the
126 /// overlay once one is open, with no second code path.
127 ///
128 /// Separate from `history` on purpose: an overlay is not a place. Opening
129 /// one pushes here and leaves history alone, and dismissing one pops here
130 /// and reveals the screen the user never left.
131 under: Vec<Layer>,
132 /// The places behind this one, most recent last.
133 ///
134 /// Requests rather than addresses, because going back means asking again
135 /// and a request is what asking takes. [`Address`](quasi_router::Address)
136 /// carries a string for a browser's address bar, which a terminal does not
137 /// have.
138 history: Vec<Request>,
139 /// The request that produced the screen currently showing.
140 here: Option<Request>,
141 /// A control waiting on its own question being answered.
142 asked: Option<(Action, Params)>,
143 /// Something to say once the screen it belongs to has arrived.
144 saying: Option<Message>,
145 }
146
147 impl Runtime {
148 /// Start on this screen, with nothing typed and nothing behind it.
149 #[must_use]
150 pub fn new(screen: Screen) -> Self {
151 let mut runtime = Self {
152 screen,
153 view: View::new(),
154 chrome: Chrome::new(),
155 under: Vec::new(),
156 history: Vec::new(),
157 here: None,
158 asked: None,
159 saying: None,
160 };
161 runtime.view.seed(&runtime.screen);
162 runtime
163 }
164
165 /// Declare what the app offers from every screen.
166 ///
167 /// Held beside the screen rather than arriving with one, which is what
168 /// makes it chrome: the bindings outlive every answer this runtime applies.
169 #[must_use]
170 pub fn with_chrome(mut self, chrome: Chrome) -> Self {
171 self.chrome = chrome;
172 self
173 }
174
175 /// Whether an overlay is open over the screen.
176 #[must_use]
177 pub const fn overlaid(&self) -> bool {
178 !self.under.is_empty()
179 }
180
181 /// The screen being shown.
182 ///
183 /// The overlay's, when one is open. That is what "being shown" means, and
184 /// it is what every key this runtime handles is working on.
185 #[must_use]
186 pub const fn screen(&self) -> &Screen {
187 &self.screen
188 }
189
190 /// What the user has done to it.
191 #[must_use]
192 pub const fn view(&self) -> &View {
193 &self.view
194 }
195
196 /// Everything reachable on it, in focus order.
197 #[must_use]
198 pub fn reaches(&self) -> Vec<Reach> {
199 crate::focus::reaches(&self.screen)
200 }
201
202 /// Whether the caret is in a field, which is what decides whether a
203 /// printable key is a shortcut or a character.
204 ///
205 /// A host wanting `q` to quit asks this first. Quitting is the host's and
206 /// not a binding here, because a key that closes the app is a fact about
207 /// the app rather than about the screen.
208 #[must_use]
209 pub fn editing(&self) -> bool {
210 self.focused().is_some_and(|spot| spot.field().is_some())
211 }
212
213 /// Whether a question is waiting to be answered.
214 #[must_use]
215 pub const fn asking(&self) -> bool {
216 self.asked.is_some()
217 }
218
219 /// Draw it.
220 pub fn draw(&self, tui: &Tui, area: Rect, buf: &mut Buffer) {
221 // What is under it first, outermost first, then this one over the top.
222 // An overlay that painted only itself would be a screen swap wearing
223 // another name.
224 for layer in &self.under {
225 tui.screen(&layer.screen, &layer.view, area, buf);
226 }
227 let area = if self.under.is_empty() {
228 area
229 } else {
230 let inset = Self::overlay_area(area);
231 // Clear what is under it inside its own bounds, so the overlay
232 // reads as being over the screen rather than mixed into it.
233 for y in inset.top()..inset.bottom() {
234 for x in inset.left()..inset.right() {
235 buf[(x, y)].reset();
236 }
237 }
238 inset
239 };
240 tui.screen(&self.screen, &self.view, area, buf);
241 }
242
243 /// Where an overlay sits inside the screen it is over.
244 ///
245 /// Inset on all four sides so the screen underneath stays visible around
246 /// it, which is the whole visual claim an overlay makes. Proportional
247 /// rather than fixed: a palette 4 rows from the edge of an 80x24 terminal
248 /// is a different thing from one 4 rows from the edge of a 200x60.
249 fn overlay_area(area: Rect) -> Rect {
250 let pad_x = (area.width / 8)
251 .max(1)
252 .min(area.width.saturating_sub(2) / 2);
253 let pad_y = (area.height / 8)
254 .max(1)
255 .min(area.height.saturating_sub(2) / 2);
256 Rect {
257 x: area.x + pad_x,
258 y: area.y + pad_y,
259 width: area.width.saturating_sub(pad_x * 2),
260 height: area.height.saturating_sub(pad_y * 2),
261 }
262 }
263
264 /// Put a message on the screen, from the host rather than from a route.
265 ///
266 /// The host has things to say that no handler knows about: a route that
267 /// failed, an address it will not open, a device that is not there. Without
268 /// this they would go to stderr, which on a terminal app is underneath the
269 /// alternate screen and therefore nowhere.
270 pub fn say(&mut self, text: impl Into<String>) {
271 self.screen.notices.push(Node::Notice {
272 kind: layout::Notice::Banner,
273 tone: layout::Tone::Danger,
274 text: text.into(),
275 });
276 }
277
278 /// What is under the caret.
279 #[must_use]
280 pub fn focused(&self) -> Option<Spot> {
281 let mut reaches = crate::focus::reaches(&self.screen);
282 if self.view.focus() >= reaches.len() {
283 return None;
284 }
285 Some(reaches.swap_remove(self.view.focus()).spot)
286 }
287
288 /// Take a key, and say what the host should do about it.
289 pub fn key(&mut self, key: Key) -> Step {
290 // A question owns the keyboard until it is answered. Anything that is
291 // not yes is no, which is the safe way round for a prompt that is only
292 // ever raised by something destructive.
293 if let Some((action, payload)) = self.asked.take() {
294 return match key {
295 // The selection was gathered when the question was raised, not
296 // now. Nothing can tick while a prompt owns the keyboard, so
297 // the two are the same set -- and reading it here would mean
298 // the answer depended on state the user could not see.
299 Key::Char('y' | 'Y') | Key::Enter => Self::send(&action, payload),
300 _ => Step::Idle,
301 };
302 }
303
304 // The app's own keys, before this runtime's table and before any
305 // screen's `Act::key`. An affordance available everywhere is not
306 // available everywhere if a screen can capture its key.
307 //
308 // Not while typing: a field has the keyboard, and a binding on a
309 // printable key would otherwise be unreachable as a character. A
310 // binding naming a key no field can consume still lands.
311 if !self.editing() || !matches!(key, Key::Char(_)) {
312 let pressed = Self::key_name(key);
313 if let Some(binding) = pressed.as_deref().and_then(|name| self.chrome.bound(name)) {
314 return Self::call(&binding.action.clone());
315 }
316 }
317
318 let reaches = crate::focus::reaches(&self.screen);
319 let count = reaches.len();
320 let here = reaches
321 .get(self.view.focus())
322 .map(|reach| reach.spot.clone());
323
324 match key {
325 Key::Tab | Key::Down => {
326 self.view.advance(1, count);
327 Step::Idle
328 }
329 Key::BackTab | Key::Up => {
330 self.view.advance(-1, count);
331 Step::Idle
332 }
333
334 Key::PageDown | Key::PageUp => {
335 // The region the caret is in, because it is the one the user is
336 // working in. A screen with focus nowhere scrolls nothing,
337 // which is honest: there is no "the pane" on a screen with
338 // several.
339 if let Some(reach) = reaches.get(self.view.focus()) {
340 let rows = if matches!(key, Key::PageDown) {
341 10
342 } else {
343 -10
344 };
345 self.view.scroll_by(&reach.region, rows);
346 }
347 Step::Idle
348 }
349
350 Key::Left | Key::Right => {
351 // The region the caret is in, when that region shows one child
352 // at a time, and otherwise the first one on the screen that
353 // does. The fallback is not a convenience: a carousel's frames
354 // are pictures, so there is nothing reachable inside one and
355 // focus can never be in it. Without this the one widget that
356 // asked for these keys could not be reached by them.
357 if let Some(slot) = self.moving(&reaches) {
358 let steps = if matches!(key, Key::Right) { 1 } else { -1 };
359 let slot = slot.clone();
360 self.view.show_by(&slot, steps);
361 }
362 Step::Idle
363 }
364
365 // An overlay first: Escape closes what is on top before it goes
366 // back, which is what Escape means everywhere else it is bound.
367 Key::Escape => {
368 if self.dismiss() {
369 Step::Idle
370 } else {
371 self.back()
372 }
373 }
374
375 Key::Backspace => {
376 if let Some(field) = here.as_ref().and_then(Spot::field) {
377 self.view.backspace(field);
378 }
379 Step::Idle
380 }
381
382 Key::Enter => match here {
383 Some(Spot::Act {
384 action,
385 confirm,
386 over,
387 ..
388 }) => {
389 let payload = self.gathering(over.as_deref());
390 match confirm {
391 Some(prompt) => {
392 self.asked = Some((action, payload));
393 Step::Ask(prompt)
394 }
395 None => Self::send(&action, payload),
396 }
397 }
398 Some(Spot::Submit { action, names }) => {
399 let payload = self.view.submission(
400 &names,
401 &reaches
402 .iter()
403 .map(|reach| reach.spot.clone())
404 .collect::<Vec<_>>(),
405 );
406 Self::send(&action, payload)
407 }
408 // A field takes Enter and does nothing with it. A browser
409 // submits the form around it, and doing that here would fire a
410 // write from the first box the user finished typing in; the
411 // submit is one Tab away and says what it does.
412 Some(Spot::Field(_)) | None => Step::Idle,
413 Some(other) => match other.enters() {
414 Some(action) => Self::call(&action.clone()),
415 None => Step::Idle,
416 },
417 },
418
419 Key::Char(' ') if !self.editing() => match here {
420 // A tick is a write when the description says it is, and
421 // staged selection when it does not. `toggle` first, because a
422 // row carrying one has said the tick *is* the write and that
423 // claim beats the screen's set.
424 Some(Spot::Row {
425 toggle: Some(action),
426 ..
427 }) => Self::call(&action),
428 // Otherwise it joins or leaves the set the screen names. The
429 // hole `5f2b8753` was filed for was here: this used to be
430 // `Step::Idle`, so the box was drawn, the key was bound, and
431 // pressing it did nothing.
432 //
433 // Still idle when a row names no value or the screen holds no
434 // set, which is the same description bug one step earlier. A
435 // key bound to nothing is what this stopped doing, so it does
436 // not start doing it again by accepting a tick that cannot be
437 // read back.
438 Some(Spot::Row {
439 ticked: Some(_),
440 value: Some(value),
441 ..
442 }) if self.screen.selection.is_some() => {
443 self.view.tick(&value);
444 Step::Idle
445 }
446 _ => Step::Idle,
447 },
448
449 Key::Char(ch) => {
450 if let Some(field) = here.as_ref().and_then(Spot::field).cloned() {
451 self.type_into(&field, ch);
452 return self.after_typing(here.as_ref());
453 }
454 // Not in a field, so the key is a shortcut if any control on
455 // the screen claimed it. `Act::key` is text rather than a
456 // modelled chord, so this is a string comparison against what
457 // the description wrote, and a name this renderer does not
458 // understand simply never matches.
459 let pressed = ch.to_string();
460 let claimed = reaches.iter().find_map(|reach| match &reach.spot {
461 Spot::Act {
462 action,
463 key: Some(key),
464 over,
465 ..
466 } if *key == pressed => Some((action.clone(), over.clone())),
467 _ => None,
468 });
469 match claimed {
470 Some((action, over)) => {
471 let payload = self.gathering(over.as_deref());
472 Self::send(&action, payload)
473 }
474 None => Step::Idle,
475 }
476 }
477 }
478 }
479
480 /// Put what the router answered onto the screen.
481 ///
482 /// Answers with a follow-up request when the response says to go somewhere
483 /// else, which the host performs the same way it performed the first one.
484 /// `request` is what was asked, because whether an answer is a place is
485 /// derived from it: a read that answered a whole screen is somewhere you
486 /// can come back to, and a write is not.
487 pub fn apply(&mut self, request: &Request, response: Response) -> Option<Request> {
488 let Response {
489 outcome,
490 notice,
491 address,
492 invalidates,
493 } = response;
494 self.saying = notice.or(self.saying.take());
495
496 match outcome {
497 // Invalidations are not applied to a whole screen, matching what an
498 // HTTP host does with them and for the same reason: every region is
499 // being replaced already, so naming one of them again says nothing
500 // the new screen does not.
501 // Over what is already there. The layer underneath is put away
502 // whole -- its screen and the view holding everything the user did
503 // to it -- and comes back untouched when the overlay is dismissed.
504 //
505 // `remember` is deliberately not called: an overlay is not a place,
506 // so history is left exactly as it was and Escape from the overlay
507 // reveals rather than navigates.
508 Outcome::Over(screen) => {
509 let under = Layer {
510 screen: std::mem::replace(&mut self.screen, screen),
511 view: std::mem::replace(&mut self.view, View::new()),
512 };
513 self.under.push(under);
514 self.view.seed(&self.screen);
515 self.announce();
516 None
517 }
518 // A whole screen replaces everything, including any overlay open
519 // over it. A route that answers with a screen is a navigation, and
520 // navigating with a palette still floating over the destination is
521 // the state nobody asked for.
522 Outcome::Screen(screen) => {
523 self.under.clear();
524 self.remember(request, address.as_ref());
525 self.screen = screen;
526 self.view.reset();
527 // The rows a new screen says are already ticked. After this the
528 // user's ticks are the truth, which is why it is applied once
529 // on arrival rather than read on every draw.
530 self.view.seed(&self.screen);
531 self.announce();
532 None
533 }
534 Outcome::Fragment { region, node } => {
535 // A region that is not there is the description bug
536 // `Screen::replace` describes, and a terminal can say so
537 // rather than swallowing it: the region it named is gone, and
538 // drawing nothing would look like a control that does nothing.
539 //
540 // The slots the answer invalidated go in the same way. On a
541 // terminal that is the whole of what invalidation means: the
542 // next frame redraws everything, so putting the new contents
543 // on the screen is putting them in front of the user. What a
544 // webview needs an out-of-band swap for, this gets for free.
545 let mut missing: Vec<String> = Vec::new();
546 if !self.screen.replace(&region, node) {
547 missing.push(region);
548 }
549 for stale in invalidates {
550 if !self.screen.replace(&stale.region, stale.node) {
551 missing.push(stale.region);
552 }
553 }
554 if !missing.is_empty() {
555 // One message naming all of them, rather than a banner per
556 // region where only the last would survive.
557 let named = missing
558 .iter()
559 .map(|region| format!("`{region}`"))
560 .collect::<Vec<_>>()
561 .join(", ");
562 let subject = if missing.len() == 1 { "is" } else { "are" };
563 self.saying = Some(Message {
564 kind: layout::Notice::Banner,
565 tone: layout::Tone::Danger,
566 text: format!("nothing on this screen {subject} called {named}"),
567 undo: None,
568 });
569 }
570 self.view.prune(&self.screen);
571 self.announce();
572 None
573 }
574 Outcome::Goto(action) => match Self::call(&action) {
575 Step::Call(request) => Some(request),
576 // An external destination is the host's to open, and there is
577 // nothing to come back for.
578 _ => None,
579 },
580 }
581 }
582
583 /// Go back, if there is anywhere to go.
584 /// A key as the text a [`Chrome`] binding names it by.
585 ///
586 /// The same string comparison `Act::key` gets, for the same reason: the
587 /// vocabulary of keys is the host's, and this host's names are these. A
588 /// modifier this renderer cannot receive is a name that never matches,
589 /// which is what a binding for another host should do here.
590 fn key_name(key: Key) -> Option<String> {
591 Some(match key {
592 Key::Char(ch) => ch.to_string(),
593 Key::Enter => "enter".into(),
594 Key::Escape => "escape".into(),
595 Key::Tab => "tab".into(),
596 Key::BackTab => "backtab".into(),
597 Key::Up => "up".into(),
598 Key::Down => "down".into(),
599 Key::PageUp => "pageup".into(),
600 Key::PageDown => "pagedown".into(),
601 Key::Backspace => "backspace".into(),
602 Key::Left => "left".into(),
603 Key::Right => "right".into(),
604 })
605 }
606
607 /// The region the arrow keys move, if the screen has one.
608 ///
609 /// The one the caret is in when that region shows one child at a time, and
610 /// otherwise the first such region in draw order. Two rules rather than one
611 /// because focus is not always a usable answer here: a carousel holds
612 /// pictures, nothing in it is reachable, and a rule that only ever asked
613 /// where the caret was would leave the widget that wanted these keys unable
614 /// to be reached by them.
615 ///
616 /// A screen with two of these and no focus in either moves the first, which
617 /// is arbitrary and is said out loud rather than hidden. Nothing in the tree
618 /// has two yet; the screen that does is the one that will want a reachable
619 /// control instead, and that is a `Spot` rather than a rule here.
620 fn moving<'a>(&'a self, reaches: &[crate::focus::Reach]) -> Option<&'a Slot> {
621 let here = reaches
622 .get(self.view.focus())
623 .and_then(|reach| self.find(&reach.region))
624 .filter(|slot| slot.showing.selective());
625 here.or_else(|| self.screen.slots.iter().find_map(Self::selective))
626 }
627
628 /// This slot or the first under it that shows one child at a time.
629 fn selective(slot: &Slot) -> Option<&Slot> {
630 if slot.showing.selective() {
631 return Some(slot);
632 }
633 slot.body.iter().find_map(|node| match node {
634 Node::Region(inner) => Self::selective(inner),
635 _ => None,
636 })
637 }
638
639 /// The slot under this address, anywhere on the screen.
640 fn find(&self, region: &str) -> Option<&Slot> {
641 self.screen.slots.iter().find_map(|slot| slot.find(region))
642 }
643
644 /// Close the overlay on top, if there is one.
645 ///
646 /// The layer underneath comes back exactly as it was left: its own focus,
647 /// its own edits, its own scroll. That is the whole reason a layer carries
648 /// its own [`View`], and history is not touched because an overlay was
649 /// never a place.
650 fn dismiss(&mut self) -> bool {
651 match self.under.pop() {
652 Some(layer) => {
653 self.screen = layer.screen;
654 self.view = layer.view;
655 true
656 }
657 None => false,
658 }
659 }
660
661 fn back(&mut self) -> Step {
662 match self.history.pop() {
663 Some(request) => {
664 self.here = Some(request.clone());
665 Step::Call(request)
666 }
667 None => Step::Idle,
668 }
669 }
670
671 /// Note where we were, before we leave it.
672 ///
673 /// The derivation the response's own documentation describes: a read that
674 /// answered a screen is a place, everything else is not, and
675 /// [`Address`](quasi_router::Address) is the override for the two cases the
676 /// derivation cannot reach.
677 fn remember(&mut self, request: &Request, address: Option<&quasi_router::Address>) {
678 let place = match address {
679 Some(quasi_router::Address::Enters(_)) => true,
680 Some(quasi_router::Address::Unchanged) => false,
681 Some(quasi_router::Address::Replaces(_)) => {
682 self.here = Some(request.clone());
683 return;
684 }
685 None => request.method == Method::Get,
686 };
687 if place && let Some(previous) = self.here.replace(request.clone()) {
688 self.history.push(previous);
689 }
690 }
691
692 /// Put whatever the response wanted said onto the screen it belongs to.
693 fn announce(&mut self) {
694 // A message's `undo` is dropped, and that is a decline rather than an
695 // oversight: `Node::Notice` has nowhere to hang a control, so the way
696 // back that the response offered has no cell to sit in. Filed.
697 if let Some(Message {
698 kind, tone, text, ..
699 }) = self.saying.take()
700 {
701 self.screen.notices.push(Node::Notice { kind, tone, text });
702 }
703 }
704
705 /// Type into a field, honouring what the description says it will take.
706 fn type_into(&mut self, field: &crate::FieldSpot, ch: char) {
707 if matches!(field.kind, layout::FieldKind::Checkbox) {
708 // A checkbox holds one of two values, so a key does not type into
709 // it: any key flips it, which is what space does to one in a
710 // browser and is the only sentence a box with two states can hear.
711 let ticked = self.view.typed(field) == Node::SELECTED;
712 let next = if ticked {
713 String::new()
714 } else {
715 Node::SELECTED.to_string()
716 };
717 self.view.set(&field.name, next);
718 return;
719 }
720
721 // `Field::max_length` is a rule the description carries and every
722 // renderer emits in its host's idiom. A browser stops accepting
723 // characters, and so does this.
724 if let Some(limit) = field.max_length
725 && self.view.typed(field).chars().count() >= limit as usize
726 {
727 return;
728 }
729 self.view.push(field, ch);
730 }
731
732 /// What a keystroke in a field costs, when the field writes as it changes.
733 fn after_typing(&mut self, here: Option<&Spot>) -> Step {
734 match here.and_then(Spot::field).and_then(|field| {
735 field
736 .changes
737 .clone()
738 .map(|action| (action, field.name.clone()))
739 }) {
740 // A field that writes on every change writes on every keystroke
741 // here, which is what `Field::changes` says and is wrong for a text
742 // box: a webview debounces on `input` and nothing in the
743 // description says a delay is allowed. Filed rather than debounced
744 // to a number this renderer made up.
745 Some((action, name)) => {
746 let value = self.view.edit(&name).unwrap_or_default().to_string();
747 let payload = Params::new().with(name, value);
748 Self::send(&action, payload)
749 }
750 None => Step::Idle,
751 }
752 }
753
754 /// The ticks a control acting over a selection sends with its call.
755 ///
756 /// Empty for a control that names no selection. A control that names one
757 /// sends the whole set, whatever it called it: see [`Act::over`] for why
758 /// the name is not matched against the screen's, which is that a webview
759 /// rendering a fragment has no screen to match it against and the two
760 /// hosts would then disagree about a typo.
761 ///
762 /// Empty is also what a control over a set nobody ticked sends, and the two
763 /// are deliberately the same. A handler receives a bulk action over
764 /// nothing, which is a case it has to handle regardless.
765 ///
766 /// [`Act::over`]: quasi_router::Act::over
767 fn gathering(&self, over: Option<&str>) -> Params {
768 let mut payload = Params::new();
769 if over.is_some() {
770 for value in self.view.ticks() {
771 payload.insert(Node::TICKED.to_owned(), value.to_owned());
772 }
773 }
774 payload
775 }
776
777 /// An action as something the host can ask.
778 fn call(action: &Action) -> Step {
779 Self::send(action, Params::new())
780 }
781
782 /// An action, plus values the control is sending that are not on it.
783 fn send(action: &Action, extra: Params) -> Step {
784 let Some(path) = action.destination.route() else {
785 return Step::Open(action.destination.as_str().to_string());
786 };
787 let mut payload = extra;
788 payload.absorb(action.params.clone());
789 Step::Call(Request {
790 method: action.method,
791 path: path.to_string(),
792 captures: Params::new(),
793 payload,
794 carried: action.carried.clone(),
795 })
796 }
797 }
798