Skip to main content

max / quasi

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