Skip to main content

max / quasi

26.7 KB · 639 lines History Blame Raw
1 //! What the user has done to a screen since it arrived.
2 //!
3 //! Two of the five things `quasi-tui`'s `View` holds, and the other three are
4 //! egui's. That is the whole difference between the two crates' state, and it is
5 //! worth saying which is which so the next reader does not go looking for the
6 //! missing ones:
7 //!
8 //! | Fact | terminal | here |
9 //! |---|---|---|
10 //! | what is typed | `View` | **`View`** |
11 //! | what is ticked | `View` | **`View`** |
12 //! | what has focus | `View` | egui's id stack |
13 //! | how far a pane is scrolled | `View` | `egui::ScrollArea` |
14 //! | where back goes | `Runtime` | [`Runtime`](crate::Runtime) |
15 //!
16 //! **Why typing is not egui's, when focus is.** egui holds widget state against
17 //! an id, and a described field is rebuilt from the description every frame; its
18 //! `TextEdit` needs a `&mut String` that outlives the frame, and the description
19 //! deliberately does not carry the value. So the buffer is the app's, held here.
20 //! That is the same conclusion `makeover-immediate`'s `Filling` reached one layer
21 //! down and for the same reason.
22
23 use std::collections::{BTreeMap, BTreeSet};
24
25 use std::time::Instant;
26
27 use quasi_router::{Action, Candidate, Node, Params, Screen};
28
29 /// The host's side of an outstanding wait.
30 ///
31 /// Two `Option`s rather than an `Option` of a pair, because they arrive at
32 /// different moments: the clock starts when the call goes out and a delivery
33 /// count arrives only if something is watching. Most waits never get one.
34 #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
35 struct Awaited {
36 /// When the call went out.
37 since: Option<Instant>,
38 /// How much has arrived, in the unit the description counted.
39 delivered: Option<u64>,
40 }
41
42 /// What a waiting consult belongs to.
43 ///
44 /// Two things ask questions while the reader works — a box about its own
45 /// value, and a region about the values inside it — and both address
46 /// themselves with a string. The strings live in different namespaces and
47 /// nothing stops them colliding, so the namespace is part of the key rather
48 /// than a convention nobody enforces.
49 #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
50 pub enum Asking {
51 /// A box's own question, by [`Field::name`](quasi_router::Field::name).
52 Field(String),
53 /// A region's question, by [`Slot::id`](quasi_router::Slot::id).
54 Region(String),
55 }
56
57 /// What the user has done to a screen since it arrived.
58 ///
59 /// A host makes one beside the screen it is holding and keeps the two together.
60 /// Empty is the honest starting state and it draws exactly what the description
61 /// says.
62 #[derive(Debug, Clone, Default, PartialEq, Eq)]
63 pub struct View {
64 /// What has been typed, by [`Field::name`](quasi_router::Field::name).
65 ///
66 /// Absent means untouched, which is different from present and empty: one
67 /// draws the description's value and the other draws a box the user has
68 /// cleared.
69 edits: BTreeMap<String, String>,
70 /// What has been ticked, by [`Row::value`](quasi_router::Row::value).
71 ticked: BTreeSet<String>,
72 /// Which branches the reader has folded or unfolded, by
73 /// [`Outline::key`](quasi_router::Outline::key).
74 ///
75 /// Absent means the description's own answer stands, so a screen arriving
76 /// with a branch shut draws it shut until the reader says otherwise.
77 opened: BTreeMap<String, bool>,
78 /// Which child each region showing one at a time is showing, by
79 /// [`Slot::id`](quasi_router::Slot::id).
80 ///
81 /// Absent means the description's own answer stands, which is `opened`'s
82 /// rule one region-kind along: a screen arriving on its second tab draws
83 /// its second tab until the reader presses something else.
84 ///
85 /// `Option<usize>` where quasi-tui's is a bare `usize`, and the difference
86 /// is real rather than a spelling. `Showing::AtMostOne` closed is `None`,
87 /// and a terminal's prev/next row cannot reach it -- there is no key that
88 /// means "close" once a strip has been walked onto a child. A summary line
89 /// here is one control that opens and shuts, so closed has to be a state
90 /// the reader can put a region back into.
91 shown: BTreeMap<String, Option<usize>>,
92 /// The value a field's [`writes`](quasi_router::Field::writes) last fired
93 /// with, by field name.
94 ///
95 /// `changes` fires when the value is complete rather than on the way to
96 /// it, so "complete" needs a baseline to be different from: a blur that
97 /// changed nothing must not write, which is what a browser's `change`
98 /// event already promises. Absent means nothing has been fired for that
99 /// field, and the description's own value is the baseline.
100 ///
101 /// Beside `edits` rather than derived from it, because they answer
102 /// different questions: `edits` is what is on screen and this is what the
103 /// route has been told.
104 written: BTreeMap<String, String>,
105 /// When a consult is due, by what asks it and the consult's place in that
106 /// asker's list.
107 ///
108 /// `N8`. The second half of the key is the cost the ruling named: a box may
109 /// ask two routes at two rates -- MNW's discover search asks its suggestion
110 /// route after 200ms and its results route after 150 -- and one deadline
111 /// per field would have the faster question cancel the slower one.
112 ///
113 /// A deadline rather than an elapsed count, so a keystroke arriving during
114 /// the wait moves it forward by overwriting the entry: that is the whole of
115 /// debouncing, and it needs no tick counter and no second clock.
116 ///
117 /// Here rather than in `Pass` because a `Pass` is one frame and the wait
118 /// spans many. It is the same reason `edits` lives here.
119 ///
120 /// Keyed by [`Asking`] rather than by a bare name, so a region and a field
121 /// that happen to share a string keep their own deadlines: a
122 /// [`Slot::id`](quasi_router::Slot::id) and a
123 /// [`Field::name`](quasi_router::Field::name) are addresses in two
124 /// namespaces and nothing keeps them apart, so a map keyed on the string
125 /// would have had a panel's recompute cancelled by a box that shared its
126 /// name.
127 awaiting: BTreeMap<(Asking, usize), Instant>,
128 /// The question the caret is owed, until the frame that gives it to it.
129 ///
130 /// [`Screen::opens_at`](quasi_router::Screen::opens_at) read once on
131 /// arrival, and taken by the field that claims it. Focus is egui's here --
132 /// the table at the top of this module says so -- so this is not the caret,
133 /// it is the one-frame instruction to ask egui for it.
134 ///
135 /// Taken rather than read, so it happens on the arrival frame and no
136 /// other. A flag left standing would ask for focus every frame and the
137 /// reader could never move the caret off the box.
138 opens: Option<String>,
139 /// The action this screen is waiting on, when one is outstanding.
140 ///
141 /// Only an action carrying [`Action::awaiting`] lands here: the
142 /// description says which calls are worth locking a control for, and this
143 /// renderer does not decide that a route is slow. Beside `awaiting` above
144 /// and not folded into it, which is the distinction the ruling turns on: a
145 /// consult is a wait this renderer chose to impose, and this is a wait the
146 /// description declared.
147 outstanding: Option<Action>,
148 /// When the outstanding call went out, and how much of it has landed.
149 ///
150 /// Wiki `loading-and-progress-standard`, rule 1. `Action::awaiting` carries
151 /// the size of the payload and nothing can carry how much of it has
152 /// arrived: that is a fact about bytes in flight, so it is the host's to
153 /// report and it lands here beside the action it belongs to.
154 ///
155 /// Both are `None` for a host that is not watching, and that is the honest
156 /// common case rather than a gap. A bar drawn out of a total alone would be
157 /// claiming somebody is counting.
158 progress: Awaited,
159 /// The candidates a field's suggestion route answered with.
160 ///
161 /// [`Field::suggests`](quasi_router::Field::suggests) is answered with a
162 /// list belonging to a control rather than a region, so there is nowhere
163 /// in the screen to land it: it lands here, beside what has been typed
164 /// into the box it belongs to.
165 ///
166 /// One at a time and named by its field, for `quasi-tui`'s reason: a list
167 /// belongs to the box being typed into, and keeping a second field's stale
168 /// candidates would draw a list under a box nobody is in.
169 suggesting: Option<(String, Vec<Candidate>)>,
170 /// How many slots a repeating question stands in, by
171 /// [`Field::name`](quasi_router::Field::name), where the reader has changed
172 /// it.
173 ///
174 /// And the same discovery as everything else here: the description says
175 /// how many answers it was given, and how many boxes there are *now* is a
176 /// fact about what the reader has done since. A browser owns it as
177 /// elements in the document, so nobody had to name it there; there is no
178 /// document here.
179 ///
180 /// Absent means the description's own count still stands, which is what
181 /// makes an untouched screen draw what the handler said.
182 slots: BTreeMap<String, usize>,
183 }
184
185 impl View {
186 /// Nothing typed and nothing ticked.
187 #[must_use]
188 pub fn new() -> Self {
189 Self::default()
190 }
191
192 /// What a field is showing: what was typed, or what the description offers.
193 #[must_use]
194 pub fn showing<'a>(&'a self, name: &str, described: Option<&'a str>) -> &'a str {
195 self.edits
196 .get(name)
197 .map_or(described.unwrap_or_default(), String::as_str)
198 }
199
200 /// How many slots this repeating question stands in right now.
201 ///
202 /// `described` is what the description offered, which is
203 /// [`Field::slots`](quasi_router::Field::slots). What the reader has added
204 /// or taken away wins, and the answer is held to the question's own floor
205 /// and ceiling so that no walk can offer a slot the description refuses.
206 #[must_use]
207 pub fn standing(&self, field: &quasi_router::Field) -> usize {
208 let Some(repeat) = &field.repeats else {
209 return 1;
210 };
211 let described = repeat.standing();
212 let count = self.slots.get(&field.name).copied().unwrap_or(described);
213 count
214 .max(repeat.least)
215 .min(repeat.most.unwrap_or(usize::MAX))
216 }
217
218 /// Add a slot to a repeating question, if its ceiling allows another.
219 ///
220 /// No request, which is the third of the three things the member is for:
221 /// the reader creates and destroys slots, and the box that appears is a box
222 /// nothing was asked for.
223 pub fn add_slot(&mut self, field: &quasi_router::Field) {
224 let Some(repeat) = &field.repeats else {
225 return;
226 };
227 let standing = self.standing(field);
228 if !repeat.more(standing) {
229 return;
230 }
231 self.slots.insert(field.name.clone(), standing + 1);
232 }
233
234 /// Take one slot out of a repeating question, if its floor allows one
235 /// fewer.
236 ///
237 /// The answers after it move up, buffer and all, because the names are
238 /// positional: leaving them where they were would submit a hole under the
239 /// name the reader had just emptied and drop the last answer off the end.
240 /// That is the same renumbering `repeat.js` does in the browser.
241 pub fn remove_slot(&mut self, field: &quasi_router::Field, at: usize) {
242 let Some(repeat) = &field.repeats else {
243 return;
244 };
245 let standing = self.standing(field);
246 if at >= standing || !repeat.fewer(standing) {
247 return;
248 }
249 // Every name the slot submits under, which is one for an ordinary
250 // repeating question and one per part for a grouped one. Read off
251 // `Field::instance_fields` rather than spelled here, so this moves the
252 // same names the drawing and the submission use.
253 for slot in at..standing - 1 {
254 for (here, next) in field
255 .instance_fields(slot)
256 .iter()
257 .zip(field.instance_fields(slot + 1).iter())
258 {
259 let carried = self
260 .edits
261 .get(&next.name)
262 .cloned()
263 .or_else(|| next.value.clone());
264 match carried {
265 Some(value) => {
266 self.edits.insert(here.name.clone(), value);
267 }
268 None => {
269 self.edits.remove(&here.name);
270 }
271 }
272 }
273 }
274 for last in field.instance_fields(standing - 1) {
275 self.edits.remove(&last.name);
276 }
277 self.slots.insert(field.name.clone(), standing - 1);
278 }
279
280 /// The buffer a text control writes through, seeded from the description.
281 ///
282 /// `&mut` because that is what an immediate-mode text control takes: there
283 /// is no DOM to read the value back out of afterwards. Seeding on first
284 /// touch rather than up front is what keeps "untouched" distinguishable
285 /// from "cleared".
286 pub fn buffer(&mut self, name: &str, described: Option<&str>) -> &mut String {
287 self.edits
288 .entry(name.to_owned())
289 .or_insert_with(|| described.unwrap_or_default().to_owned())
290 }
291
292 /// What has been typed into a field, if anything has.
293 #[must_use]
294 pub fn edit(&self, name: &str) -> Option<&str> {
295 self.edits.get(name).map(String::as_str)
296 }
297
298 /// Put a value in, as a host restoring one would.
299 pub fn set(&mut self, name: impl Into<String>, value: impl Into<String>) {
300 self.edits.insert(name.into(), value.into());
301 }
302
303 /// Whether this value is one the field's `changes` has already fired with.
304 ///
305 /// The baseline a completed value is compared against. Falls back to what
306 /// the description offered, so the first blur on a box nobody touched
307 /// writes nothing.
308 #[must_use]
309 pub fn unwritten(&self, name: &str, value: &str, described: Option<&str>) -> bool {
310 match self.written.get(name) {
311 Some(written) => written != value,
312 None => described.unwrap_or_default() != value,
313 }
314 }
315
316 /// Remember what a field's `changes` fired with.
317 pub fn wrote(&mut self, name: &str, value: &str) {
318 self.written.insert(name.to_owned(), value.to_owned());
319 }
320
321 /// The open suggestion list, if it belongs to this field.
322 #[must_use]
323 pub fn suggesting(&self, name: &str) -> Option<&[Candidate]> {
324 self.suggesting
325 .as_ref()
326 .filter(|(field, _)| field == name)
327 .map(|(_, options)| options.as_slice())
328 }
329
330 /// What a field's suggestion route answered with.
331 ///
332 /// An empty answer closes the list rather than opening an empty one: a
333 /// route with nothing to suggest and a route that was never asked leave the
334 /// screen in the same state.
335 pub fn suggested(&mut self, name: impl Into<String>, options: Vec<Candidate>) {
336 let name = name.into();
337 self.suggesting = (!options.is_empty()).then_some((name, options));
338 }
339
340 /// Put the list away.
341 pub fn unsuggest(&mut self) {
342 self.suggesting = None;
343 }
344
345 /// Start, or restart, the wait before a consult is asked.
346 ///
347 /// Called on every keystroke, which is what makes it a debounce rather
348 /// than a delay: the second keystroke overwrites the first one's deadline,
349 /// so a name typed in one go is asked about once. A region's question is
350 /// restarted by a keystroke in any of the boxes it contains, which is the
351 /// same sentence one turn out.
352 pub fn wait_to_consult(&mut self, asked: Asking, at: usize, until: Instant) {
353 self.awaiting.insert((asked, at), until);
354 }
355
356 /// When this consult is due, if one is waiting.
357 #[must_use]
358 pub fn consult_due(&self, asked: Asking, at: usize) -> Option<Instant> {
359 self.awaiting.get(&(asked, at)).copied()
360 }
361
362 /// Forget the wait, because the question has now been asked.
363 pub fn consulted(&mut self, asked: Asking, at: usize) {
364 self.awaiting.remove(&(asked, at));
365 }
366
367 /// The values other controls contribute to a question, by name.
368 ///
369 /// [`Consult::sends`](quasi_router::Consult::sends). Read out of what has
370 /// been drawn, which is where a described field's own offer lands the first
371 /// time it is drawn, so an untouched filter still sends what it is showing.
372 ///
373 /// A name nothing on the screen carries contributes nothing rather than an
374 /// empty value, so a route can tell "not on this screen" from "on it and
375 /// blank".
376 #[must_use]
377 pub fn contributed(&self, names: &[String]) -> Params {
378 let mut params = Params::new();
379 for name in names {
380 if let Some(value) = self.edits.get(name) {
381 params = params.with(name.clone(), value.clone());
382 }
383 }
384 params
385 }
386
387 /// The action this view is waiting on.
388 #[must_use]
389 pub const fn outstanding(&self) -> Option<&Action> {
390 self.outstanding.as_ref()
391 }
392
393 /// Say that this action is outstanding, or that nothing is.
394 ///
395 /// The runtime's to set, from what the description marked as awaiting.
396 ///
397 /// Starts the clock, and clears whatever the last wait had delivered. A
398 /// wait's elapsed time is the one time value it may show, and it is only
399 /// honest if it belongs to this call rather than to the one before it.
400 pub(crate) fn await_on(&mut self, action: Option<Action>) {
401 self.await_on_at(action, Instant::now());
402 }
403
404 /// [`await_on`](Self::await_on) with the clock handed in, for tests.
405 pub(crate) fn await_on_at(&mut self, action: Option<Action>, now: Instant) {
406 self.progress = Awaited {
407 since: action.as_ref().map(|_| now),
408 delivered: None,
409 };
410 self.outstanding = action;
411 }
412
413 /// Say how much of the outstanding call has arrived.
414 ///
415 /// The host's to call, as often as it likes, from whatever it is watching:
416 /// bytes off a socket, rows out of an import. In the unit the description
417 /// counted, which is the app's business either way -- see
418 /// `makeover_layout::Awaiting::amount`.
419 ///
420 /// Ignored when nothing is outstanding. A delivery count with no wait
421 /// attached would be drawn against the next call to go out, which is a
422 /// number belonging to the wrong wait.
423 pub fn delivered(&mut self, amount: u64) {
424 if self.outstanding.is_some() {
425 self.progress.delivered = Some(amount);
426 }
427 }
428
429 /// What is known about the wait that is running, at `now`.
430 ///
431 /// Empty when nothing is outstanding, which is what makes the drawing
432 /// degrade to the activity mark rather than to a bar of nothing.
433 #[must_use]
434 pub fn progress_at(&self, now: Instant) -> makeover_immediate::widget::Progress {
435 makeover_immediate::widget::Progress {
436 delivered: self.progress.delivered,
437 elapsed: self
438 .progress
439 .since
440 .map(|since| now.saturating_duration_since(since)),
441 }
442 }
443
444 /// Whether this is the control that was pressed and has not been answered.
445 ///
446 /// What the drawing consults to disable it, which in an immediate-mode
447 /// renderer is also the whole of the guard: a disabled control reports no
448 /// click, so the second press does not exist rather than being discarded.
449 #[must_use]
450 pub fn busy(&self, action: &Action) -> bool {
451 self.outstanding.as_ref() == Some(action)
452 }
453
454 /// Whether a branch is open, given what its description says.
455 ///
456 /// The description says where the outline starts and the reader says where
457 /// it is now, the way the shipped egui sidebar this describes already
458 /// worked -- expansion was egui's own state and the app never held it. See
459 /// [`Row::open`](quasi_router::Row::open) for why folding a branch asks
460 /// the app nothing.
461 #[must_use]
462 pub fn open(&self, key: &str, described: bool) -> bool {
463 self.opened.get(key).copied().unwrap_or(described)
464 }
465
466 /// Fold an open branch, or open a shut one.
467 pub fn fold(&mut self, key: &str, described: bool) {
468 let open = self.open(key, described);
469 self.opened.insert(key.to_string(), !open);
470 }
471
472 /// Which child a region is showing, given what its description says.
473 ///
474 /// [`open`](Self::open)'s shape, and the same split: the description says
475 /// which child a screen arrives on and the reader says which one it is on
476 /// now. Clamped to the body, for [`Slot::current`](quasi_router::Slot::current)'s reason -- a stale
477 /// index left behind by a screen with more children would report a region
478 /// that vanished.
479 #[must_use]
480 pub fn shown(&self, slot: &quasi_router::Slot) -> Option<usize> {
481 match self.shown.get(&slot.id) {
482 Some(at) => at.map(|at| at.min(slot.body.len().saturating_sub(1))),
483 None => slot.current(),
484 }
485 }
486
487 /// Show a particular child of a region.
488 pub fn show(&mut self, region: &str, at: usize) {
489 self.shown.insert(region.to_string(), Some(at));
490 }
491
492 /// Move a region to another of its children, wrapping at both ends.
493 ///
494 /// quasi-tui's rule, and for its reason: neither renderer has anything to
495 /// show you that you are on the last frame, so a next control that stops
496 /// dead reads as a broken control rather than as the end of the gallery.
497 ///
498 /// A closed region opens on its first child, which is the only reading of
499 /// "next" that does anything from closed.
500 pub fn show_by(&mut self, slot: &quasi_router::Slot, steps: isize) {
501 let count = slot.body.len();
502 if count == 0 {
503 return;
504 }
505 let at = match self.shown(slot) {
506 Some(at) => (at as isize + steps).rem_euclid(count as isize) as usize,
507 None => 0,
508 };
509 self.show(&slot.id, at);
510 }
511
512 /// Open a shut disclosure, or shut an open one.
513 ///
514 /// Only reachable for [`Showing::AtMostOne`](quasi_router::layout::Showing::AtMostOne):
515 /// closing is what that member means and nothing else can be put into the
516 /// closed state.
517 pub fn disclose(&mut self, slot: &quasi_router::Slot) {
518 let next = self.shown(slot).is_none().then_some(0);
519 self.shown.insert(slot.id.clone(), next);
520 }
521
522 /// Whether a row's value is in the screen's selection.
523 #[must_use]
524 pub fn is_ticked(&self, value: &str) -> bool {
525 self.ticked.contains(value)
526 }
527
528 /// Add or remove a row's value from the selection.
529 pub fn tick(&mut self, value: &str) {
530 if !self.ticked.remove(value) {
531 self.ticked.insert(value.to_owned());
532 }
533 }
534
535 /// Everything ticked, in a stable order.
536 pub fn ticks(&self) -> impl Iterator<Item = &str> {
537 self.ticked.iter().map(String::as_str)
538 }
539
540 /// The rows a new screen says are already ticked.
541 ///
542 /// Applied once on arrival rather than read on every draw: after this the
543 /// user's ticks are the truth, and a description that kept overriding them
544 /// would undo a tick the moment anything redrew.
545 ///
546 /// This walked `Node::List` alone until the 2026-09-06 collapse, so a
547 /// *table* arriving with rows already ticked seeded nothing and the ticks
548 /// drew empty. One node now, and one walk reaches both arrangements.
549 pub fn seed(&mut self, screen: &Screen) {
550 for slot in &screen.slots {
551 for placed in slot.body.iter() {
552 if let Node::Table { rows, .. } = &placed.node {
553 for row in rows {
554 if let (Some(true), Some(value)) = (row.selected, row.value.as_ref()) {
555 self.ticked.insert(value.clone());
556 }
557 }
558 }
559 }
560 }
561 }
562
563 /// Owe the caret to the question this screen opens at.
564 ///
565 /// Arrival behaviour, beside [`seed`](Self::seed) and for its reason: a
566 /// screen cannot pull the caret back on a redraw, so this is read once and
567 /// spent on the next frame. See
568 /// [`Screen::opens_at`](quasi_router::Screen::opens_at).
569 pub fn open_at(&mut self, screen: &Screen) {
570 self.opens.clone_from(&screen.opens_at);
571 }
572
573 /// Whether this box is the one owed the caret, taking the claim with it.
574 ///
575 /// Taken rather than read: the frame that asks egui for focus is the only
576 /// frame that should, and a claim two boxes could both answer would be two
577 /// widgets fighting over the caret every frame.
578 pub(crate) fn claims_caret(&mut self, name: &str) -> bool {
579 if self.opens.as_deref() == Some(name) {
580 self.opens = None;
581 return true;
582 }
583 false
584 }
585
586 /// Forget everything, for a screen that has been replaced.
587 ///
588 /// The outstanding consults go with the rest. A wait that survived the
589 /// screen would ask about a field that is no longer on it, and land the
590 /// answer in a region that no longer exists.
591 pub fn reset(&mut self) {
592 self.edits.clear();
593 self.ticked.clear();
594 // What the last screen was owed. The next one says for itself, and a
595 // name carried across would put the caret in whatever box on the new
596 // screen happened to share it.
597 self.opens = None;
598 // Which frame a region was on belongs to the screen it was on, and a
599 // region id is only unique within one. quasi-tui clears this here for
600 // the same reason.
601 self.shown.clear();
602 self.awaiting.clear();
603 self.suggesting = None;
604 // A screen that has arrived is the answer to whatever was outstanding,
605 // or is somewhere else entirely.
606 self.outstanding = None;
607 self.progress = Awaited::default();
608 }
609
610 /// The values a form submits, by the names it declared.
611 ///
612 /// Every declared name is sent, including the ones nothing was typed into,
613 /// because a form that omits an untouched field is a form that cannot clear
614 /// one. What is sent for those is whatever the description offered.
615 #[must_use]
616 pub fn submission(&self, names: &[String], described: &BTreeMap<String, String>) -> Params {
617 let mut params = Params::new();
618 for name in names {
619 let value = self
620 .edits
621 .get(name)
622 .or_else(|| described.get(name))
623 .map_or("", String::as_str);
624 params = params.with(name.clone(), value.to_owned());
625 }
626 params
627 }
628
629 /// What the screen's selection sends with an action taken over it.
630 #[must_use]
631 pub fn gathering(&self, under: &str) -> Params {
632 let mut params = Params::new();
633 for value in self.ticks() {
634 params = params.with(under.to_owned(), value.to_owned());
635 }
636 params
637 }
638 }
639