//! The state a terminal owns because nothing else will.
//!
//! This type is the answer to `39057019`, and the finding is worth restating
//! because the answer only makes sense next to it. `Field::value` is what a
//! handler re-offers after a refused write. It is not what is in the box right
//! now, and for a [`layout::FieldKind::Secret`] it is nothing at all, on
//! purpose: a password that comes back down the wire is a password in a page
//! and in a proxy log. A browser never made anyone notice, because a browser
//! owns the contents of an `` and redraws it on every keystroke without
//! asking the description for permission.
//!
//! A terminal owns nothing. So the drawing of an editable screen is not a
//! function of the description alone, and the two ways to admit that were:
//! hand the renderer a second argument, or have the runtime rewrite the
//! description before drawing it.
//!
//! **The second argument won.** Rewriting keeps [`crate::Tui`] a pure function
//! of one argument by making the runtime lie about what the handler said, and
//! the lie is not free: `Field::value` refuses to hold a secret, so a runtime
//! that wrote the typed password into the description would have had to defeat
//! that refusal to draw the dots. The guarantee that no renderer emits a secret
//! is worth more than the pure signature, and this way the two facts stay
//! separate: the description says what the server offers, and this says what the
//! user has done since.
//!
//! Once it exists it holds the rest of what the browser was quietly providing,
//! because it turns out to be the same discovery four times: what is typed,
//! what has focus, how far a pane is scrolled, and where the back button goes.
//! None of the four is in a description and none of them should be.
//!
//! An overlay holds a second one of these rather than a fifth field being added
//! to this one. `Outcome::Over` draws a whole screen over another, and the
//! screen underneath keeps its own reach, focus, edits and scroll while it is
//! covered: sharing one `View` between the two would mean dismissing a palette
//! took the user's typing and scroll position with it. See `Runtime`'s `under`
//! stack, which holds the pair.
//!
//! One more is of the same kind and is deliberately not held here: where the
//! caret sits inside a field. It belongs on this list by nature, and it is
//! absent because no described screen needs it yet: the one measured consumer
//! is goingson's `search.js`, whose completion list depends on which token the
//! caret is inside, and that file stays JS. Saying so here keeps the boundary
//! explicit, so the next screen that wants caret-dependent completion knows
//! this is where it would land rather than re-asking whether a description
//! should carry one. It should not.
use std::collections::{BTreeMap, BTreeSet};
use std::time::Instant;
use makeover_layout as layout;
use quasi_router::{Action, Candidate, Chrome, Frame, Params, Screen};
use crate::focus::{FieldSpot, Spot};
use crate::{Hidden, Local};
/// The host's side of an outstanding wait.
///
/// Two `Option`s rather than an `Option` of a pair, because they arrive at
/// different moments: the clock starts when the call goes out and a delivery
/// count arrives only if something is watching. Most waits never get one.
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
struct Awaited {
/// When the call went out.
since: Option,
/// How much has arrived, in the unit the description counted.
delivered: Option,
}
/// What the user has done to a screen since it arrived.
///
/// A host makes one beside the screen it is holding and keeps the two together.
/// Empty is the honest starting state and it draws exactly what the description
/// says, which is what every test that predates this passes.
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct View {
/// What has been typed, by [`Field::name`](quasi_router::Field::name).
///
/// Absent means untouched, which is different from present and empty: one
/// draws the description's value and the other draws a box the user has
/// cleared.
edits: BTreeMap,
/// The value a field's [`writes`](quasi_router::Field::writes) last fired
/// with, by field name.
///
/// `changes` fires when the value is complete rather than on every
/// keystroke, so "complete" needs a baseline: leaving a box nobody altered
/// must not write, which is what a browser's `change` already promises and
/// what `quasi-webview` therefore already does.
///
/// Beside `edits` rather than derived from it, because they answer different
/// questions: `edits` is what is in the box and this is what the route has
/// been told.
written: BTreeMap,
/// Which reached thing has focus, as an index into [`crate::focus::spots`].
///
/// Focus is this renderer's and lives here rather than in a description,
/// which is why it survives a redraw: reach is recomputed from the screen,
/// focus is a fact about where the user has walked. See
/// [`crate::focus`]'s header for the three terms.
focus: usize,
/// How far each region has been scrolled, in rows, by
/// [`Slot::id`](quasi_router::Slot::id).
scroll: BTreeMap,
/// What has been ticked, by [`Row::value`](quasi_router::Row::value).
///
/// The fifth thing the browser was quietly providing, and it arrived last
/// because it is the one a browser does *not* fully provide: a checkbox
/// owns its own checked state, but nothing gathers the boxes back up, so
/// every app wrote that part by hand. Here there is no checkbox to own
/// anything, which is what made the hole visible.
///
/// A set rather than a map from name to bool. Absent is not ticked, and
/// the two spellings of that would otherwise drift.
///
/// Only the current screen's set, because a screen names one
/// ([`Screen::selection`](quasi_router::Screen::selection)) and a new
/// screen is a new set. Which set it is does not need storing: the screen
/// beside this one says.
ticked: BTreeSet,
/// Which branches the reader has folded or unfolded, by
/// [`Outline::key`](quasi_router::Outline::key).
///
/// And the same discovery as [`shown`](Self::shown) one node kind along:
/// the description says a branch is open and that is where the outline
/// starts, not where it stays. A browser owns this too and never made
/// anyone notice, because folding a row there is an attribute on markup
/// the document already holds.
///
/// Absent means the description's own answer still stands, so a screen
/// arriving with a branch shut draws it shut until the reader says
/// otherwise.
opened: BTreeMap,
/// Which child each region is showing, by
/// [`Slot::id`](quasi_router::Slot::id).
///
/// And the sixth of the same discovery. A description says a region shows
/// one of its children at a time and says which one it started on; where
/// the reader has moved to since is this renderer's, exactly as
/// [`scroll`](Self::scroll) is. A browser owns this too and never made
/// anyone notice, because moving a carousel there is a class on an element
/// the document already holds.
///
/// Absent means the description's own answer still stands, which is what
/// makes an untouched screen draw what the handler said.
shown: BTreeMap,
/// The action this screen is waiting on, when one is outstanding.
///
/// And the seventh of the same discovery: a control that has been pressed
/// and has not been answered yet is a fact about this moment, which is why
/// it lives here and not in the description. A browser owns it too, and
/// htmx expresses it as a class on the element that made the request.
///
/// Only an [`Action`] carrying [`Action::awaiting`] ever lands here. The
/// rest resolve fast enough that a terminal drawing them busy would be a
/// flicker, and the description says which those are.
outstanding: Option,
/// When the outstanding call went out, and how much of it has landed.
///
/// Wiki `loading-and-progress-standard`, rule 1. `Action::awaiting` carries
/// the size of the payload; nothing can describe how much of it has arrived,
/// because that is a fact about bytes in flight. So it is the host's to
/// report and it lands here beside the action it belongs to.
///
/// Both `None` for a host that is not watching, which is the honest common
/// case rather than a gap. A bar drawn out of a total alone would be
/// claiming somebody is counting.
progress: Awaited,
/// Which control inside the focused table row the caret has stepped onto,
/// as an index into [`Spot::Row`]'s `inside`.
///
/// And the eighth of the same discovery. `None` is the ordinary state: the
/// caret is on the row itself, which is every stop on every other kind of
/// node. A table row is the one stop that has an inside, because
/// `makeover_tui::table` answers no coordinates back and its cells
/// therefore cannot be stops of their own; see [`crate::focus`]'s header.
///
/// Held beside [`focus`](Self::focus) rather than folded into it, so that
/// nothing counting reachable things has to know a table is different. Every
/// walk in the crate still sees one number per row.
inside: Option,
/// The candidates a field's suggestion route answered with, and which one
/// the caret is on.
///
/// And the ninth of the same discovery this type's header lists: what a
/// route has just suggested is a fact about this moment, not about the
/// screen the handler described. A browser owns it as a list of elements
/// in the document; a terminal owns nothing, so it is here.
///
/// One at a time, keyed by field name rather than a map of every field's
/// list. A list belongs to the box being typed into, and there is one of
/// those: keeping a second field's stale candidates would draw a list under
/// a box nobody is in.
suggesting: Option,
/// How many slots a repeating question stands in, by
/// [`Field::name`](quasi_router::Field::name), where the reader has changed
/// it.
///
/// And the same discovery as everything else here: the description says
/// how many answers it was given, and how many boxes there are *now* is a
/// fact about what the reader has done since. A browser owns it as
/// elements in the document, so nobody had to name it there; there is no
/// document here.
///
/// Absent means the description's own count still stands, which is what
/// makes an untouched screen draw what the handler said.
slots: BTreeMap,
}
/// One field's open suggestion list.
///
/// [`View::suggesting`]. The candidates as the route answered them, plus which
/// one the caret is on — nothing else, because everything else about the list
/// is the description's or the drawing's.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Suggesting {
/// The [`Field::name`](quasi_router::Field::name) whose list this is.
pub field: String,
/// The candidates, in the order the route offered them.
pub options: Vec,
/// Which one the caret is on, or `None` before an arrow has been pressed.
///
/// Nothing highlighted is the state a list arrives in, and it is why Enter
/// belongs to the form until an arrow has been pressed: a list that
/// highlighted its first entry on arrival would take the Enter that submits
/// a form the moment the user paused typing.
pub at: Option,
}
impl View {
/// Nothing typed, the first thing focused, nothing scrolled.
#[must_use]
pub fn new() -> Self {
Self::default()
}
/// How many slots this repeating question stands in right now.
///
/// `described` is what the description offered, which is
/// [`Field::slots`](quasi_router::Field::slots). What the reader has added
/// or taken away wins, and the answer is held to the question's own floor
/// and ceiling so that no walk can offer a slot the description refuses.
#[must_use]
pub fn standing(&self, field: &quasi_router::Field) -> usize {
let Some(repeat) = &field.repeats else {
return 1;
};
let described = repeat.standing();
let count = self.slots.get(&field.name).copied().unwrap_or(described);
count
.max(repeat.least)
.min(repeat.most.unwrap_or(usize::MAX))
}
/// Add a slot to a repeating question, if its ceiling allows another.
///
/// No request, which is the third of the three things the member is for:
/// the reader creates and destroys slots, and the box that appears is a box
/// nothing was asked for.
pub fn add_slot(&mut self, field: &quasi_router::Field) {
let Some(repeat) = &field.repeats else {
return;
};
let standing = self.standing(field);
if !repeat.more(standing) {
return;
}
self.slots.insert(field.name.clone(), standing + 1);
}
/// Take one slot out of a repeating question, if its floor allows one
/// fewer.
///
/// The answers after it move up, buffer and all, because the names are
/// positional: leaving them where they were would submit a hole under the
/// name the reader had just emptied and drop the last answer off the end.
/// That is the same renumbering `repeat.js` does in the browser.
pub fn remove_slot(&mut self, field: &quasi_router::Field, at: usize) {
let Some(repeat) = &field.repeats else {
return;
};
let standing = self.standing(field);
if at >= standing || !repeat.fewer(standing) {
return;
}
// Every name the slot submits under, which is one for an ordinary
// repeating question and one per part for a grouped one. Read off
// `Field::instance_fields` rather than spelled here, so this moves the
// same names the drawing and the caret walk use.
for slot in at..standing - 1 {
for (here, next) in field
.instance_fields(slot)
.iter()
.zip(field.instance_fields(slot + 1).iter())
{
let carried = self
.edits
.get(&next.name)
.cloned()
.or_else(|| next.value.clone());
match carried {
Some(value) => {
self.edits.insert(here.name.clone(), value);
}
None => {
self.edits.remove(&here.name);
}
}
}
}
for last in field.instance_fields(standing - 1) {
self.edits.remove(&last.name);
}
self.slots.insert(field.name.clone(), standing - 1);
}
/// What is in the box: what has been typed, or what the description offers,
/// or nothing.
///
/// The order is the whole of the type's job. An untouched field shows what
/// the handler put there; a touched one shows what the user did, including
/// when what they did was empty it.
#[must_use]
pub fn typed<'a>(&'a self, field: &'a FieldSpot) -> &'a str {
self.showing(&field.name, field.value.as_deref())
}
/// The action this view is waiting on.
///
/// What the drawing consults to mute a control that has been pressed, and
/// what the runtime consults to refuse a second press of it.
#[must_use]
pub const fn outstanding(&self) -> Option<&Action> {
self.outstanding.as_ref()
}
/// Say that this action is outstanding, or that nothing is.
///
/// The runtime's to set. A host driving this crate without one is drawing a
/// screen it never dispatched from, so there is nothing for it to say here.
pub(crate) fn awaiting(&mut self, action: Option) {
self.awaiting_at(action, Instant::now());
}
/// [`awaiting`](Self::awaiting) with the clock handed in, for tests.
pub(crate) fn awaiting_at(&mut self, action: Option, now: Instant) {
self.progress = Awaited {
since: action.as_ref().map(|_| now),
delivered: None,
};
self.outstanding = action;
}
/// Say how much of the outstanding call has arrived.
///
/// The host's to call, as often as it likes, from whatever it is watching:
/// bytes off a socket, rows out of an import. In the unit the description
/// counted, which is the app's business either way -- see
/// `makeover_layout::Awaiting::amount`.
///
/// Ignored when nothing is outstanding. A delivery count with no wait
/// attached would be drawn against the next call to go out, which is a
/// number belonging to the wrong wait.
pub fn delivered(&mut self, amount: u64) {
if self.outstanding.is_some() {
self.progress.delivered = Some(amount);
}
}
/// What is known about the wait that is running, at `now`.
///
/// Empty when nothing is outstanding, which is what makes the drawing
/// degrade to the activity mark rather than to a bar of nothing.
#[must_use]
pub fn progress_at(&self, now: Instant) -> makeover_tui::piece::Progress {
makeover_tui::piece::Progress {
delivered: self.progress.delivered,
elapsed: self
.progress
.since
.map(|since| now.saturating_duration_since(since)),
}
}
/// Whether this is the control that was pressed and has not been answered.
#[must_use]
pub fn busy(&self, action: &Action) -> bool {
self.outstanding.as_ref() == Some(action)
}
/// [`typed`](Self::typed) for a caller holding the described field itself
/// rather than a walk's record of it, which is what the drawing has.
#[must_use]
pub fn showing<'a>(&'a self, name: &str, described: Option<&'a str>) -> &'a str {
self.edits
.get(name)
.map(String::as_str)
.or(described)
.unwrap_or_default()
}
/// What has been typed into a field by name, if anything has.
#[must_use]
pub fn edit(&self, name: &str) -> Option<&str> {
self.edits.get(name).map(String::as_str)
}
/// The open suggestion list, if it belongs to this field.
///
/// Named rather than returned bare so a caller cannot draw one field's
/// candidates under another's box.
#[must_use]
pub fn suggesting(&self, name: &str) -> Option<&Suggesting> {
self.suggesting.as_ref().filter(|open| open.field == name)
}
/// The open suggestion list, whichever field owns it.
///
/// What the drawing reads once the field that owns it has said where the
/// list goes. [`suggesting`](Self::suggesting) is the question a caller
/// holding a field asks; this is the one the drawing asks afterwards, and
/// keeping them apart is what stops a list being drawn under the wrong box.
#[must_use]
pub fn suggesting_here(&self) -> Option<&Suggesting> {
self.suggesting.as_ref()
}
/// What a field's suggestion route answered with.
///
/// Replaces whatever was open, including a list belonging to another field:
/// an answer is about the box being typed into, and the previous one is
/// about a box the user has left. Highlight starts at nothing, for
/// [`Suggesting::at`]'s reason.
///
/// An empty answer closes the list rather than opening an empty one. A
/// route with nothing to suggest and a route that was never asked leave the
/// screen in the same state, which is the honest reading of both.
pub(crate) fn suggested(&mut self, field: impl Into, options: Vec) {
let field = field.into();
self.suggesting = (!options.is_empty()).then_some(Suggesting {
field,
options,
at: None,
});
}
/// Move the highlight, wrapping, and skipping nothing.
///
/// From nothing, one step forward lands on the first candidate and one back
/// on the last, which is the terminal's own idiom and the webview's
/// program's.
pub(crate) fn highlight(&mut self, by: isize) {
let Some(open) = self.suggesting.as_mut() else {
return;
};
let count = open.options.len();
if count == 0 {
return;
}
let from = match open.at {
Some(at) => at as isize,
None if by > 0 => -1,
None => 0,
};
let there = (from + by).rem_euclid(count as isize);
open.at = Some(there as usize);
}
/// Take the highlighted candidate, closing the list.
///
/// `None` only when nothing is highlighted, which leaves the list open.
///
/// The whole candidate rather than its value, because what picking does is
/// not always to write: [`Candidate::picks`] carries an action, and only
/// the caller can perform one. Absent one the caller writes
/// [`Candidate::value`], which is the default and every site that exists
/// today.
///
/// [`Candidate`] carries no `unavailable`: a suggestion that cannot be
/// picked is a row a route should not have offered.
pub(crate) fn pick(&mut self) -> Option {
let open = self.suggesting.as_ref()?;
let candidate = open.options.get(open.at?)?.clone();
self.suggesting = None;
Some(candidate)
}
/// Put the list away.
pub(crate) fn unsuggest(&mut self) {
self.suggesting = None;
}
/// Put a value in a box.
pub fn set(&mut self, name: impl Into, value: impl Into) {
self.edits.insert(name.into(), value.into());
}
/// Whether this value is one the field's `changes` has already fired with.
///
/// Falls back to what the description offered, so walking through a box
/// without altering it writes nothing.
#[must_use]
pub fn unwritten(&self, name: &str, value: &str, described: Option<&str>) -> bool {
match self.written.get(name) {
Some(written) => written != value,
None => described.unwrap_or_default() != value,
}
}
/// Remember what a field's `changes` fired with.
pub fn wrote(&mut self, name: &str, value: &str) {
self.written.insert(name.to_owned(), value.to_owned());
}
/// Add a character to a box, starting from whatever is showing in it.
pub fn push(&mut self, field: &FieldSpot, ch: char) {
let mut value = self.typed(field).to_string();
value.push(ch);
self.set(&field.name, value);
}
/// Take the last character back out of a box.
pub fn backspace(&mut self, field: &FieldSpot) {
let mut value = self.typed(field).to_string();
value.pop();
self.set(&field.name, value);
}
/// Which reachable thing has focus.
#[must_use]
pub const fn focus(&self) -> usize {
self.focus
}
/// Move focus by `steps`, wrapping at both ends.
///
/// Wrapping rather than stopping, because a terminal has no scrollbar to
/// tell you that you are at the end of the reachable things and pressing tab
/// against a dead stop reads as a broken key.
pub fn advance(&mut self, steps: isize, reachable: usize) {
// Leaving the row leaves what is inside it. Stepping in is a move
// within one stop, so moving to another stop cannot carry it along.
self.inside = None;
if reachable == 0 {
self.focus = 0;
return;
}
let count = reachable as isize;
let at = self.focus.min(reachable - 1) as isize;
self.focus = (at + steps).rem_euclid(count) as usize;
}
/// Focus something in particular, if it is there.
pub fn focus_on(&mut self, at: usize, reachable: usize) {
if at < reachable {
self.focus = at;
self.inside = None;
}
}
/// Which control inside the focused table row the caret has stepped onto.
///
/// `None` on every other stop, and on a row the caret is standing on
/// without having stepped in. See [`crate::focus`]'s header for why a table
/// row is the one stop with an inside.
#[must_use]
pub const fn inside(&self) -> Option {
self.inside
}
/// Step into the focused row, or along the controls in it, wrapping at both
/// ends.
///
/// The first step in lands on the first control going forward and on the
/// last going back, which is what stepping into a run means from either
/// side. After that it cycles, for [`advance`](Self::advance)'s reason: a
/// terminal has nothing to show you that you are at the last control, so a
/// key that stops dead reads as a broken key. Leaving is
/// [`leave`](Self::leave)'s, on a key of its own.
pub fn step_inside(&mut self, steps: isize, controls: usize) {
if controls == 0 {
self.inside = None;
return;
}
let count = controls as isize;
let at = match self.inside {
Some(at) => (at.min(controls - 1) as isize + steps).rem_euclid(count),
None if steps < 0 => count - 1,
None => 0,
};
self.inside = Some(at as usize);
}
/// Step back out of a row, and say whether the caret was in one.
///
/// The answer is what lets Escape mean one thing at a time: it leaves the
/// row if the caret is inside one, and otherwise it goes on to mean what it
/// meant before.
pub fn leave(&mut self) -> bool {
self.inside.take().is_some()
}
/// How far a region has been scrolled.
#[must_use]
pub fn scroll(&self, region: &str) -> u16 {
self.scroll.get(region).copied().unwrap_or(0)
}
/// Scroll a region, never above its top.
///
/// There is no bottom stop here, and that is deliberate: how far a region
/// can scroll is how tall its content is at the width it was given, which
/// is a fact the drawing knows and this does not. [`crate::Tui::clamp`] is
/// where it gets trimmed, once per draw, with the rect in hand.
pub fn scroll_by(&mut self, region: &str, rows: i32) {
let at = i32::from(self.scroll(region));
let next = u16::try_from((at + rows).max(0)).unwrap_or(u16::MAX);
self.scroll.insert(region.to_string(), next);
}
/// Hold a region at this offset.
pub fn scrolled_to(&mut self, region: &str, rows: u16) {
self.scroll.insert(region.to_string(), rows);
}
/// Which child a region is showing, given what its description says.
///
/// [`scroll`](Self::scroll)'s shape with one difference: a scroll has an
/// obvious zero and this does not, so the description's own answer is the
/// floor rather than the top of the region.
#[must_use]
pub fn shown(&self, slot: &quasi_router::Slot) -> Option {
match self.shown.get(&slot.id) {
Some(at) => Some((*at).min(slot.body.len().saturating_sub(1))),
None => slot.current(),
}
}
/// Move a region to another of its children, wrapping at both ends.
///
/// Wrapping for [`advance`](Self::advance)'s reason: a terminal has nothing
/// to show you that you are at the last frame, so a next key that stops
/// dead reads as a broken key rather than as the end of the gallery.
///
/// A closed dismissible region opens on its first child, which is the only
/// reading of "next" that does anything from closed.
pub fn show_by(&mut self, slot: &quasi_router::Slot, steps: isize) {
let count = slot.body.len();
if count == 0 {
return;
}
let at = match self.shown(slot) {
Some(at) => (at as isize + steps).rem_euclid(count as isize) as usize,
None => 0,
};
self.shown.insert(slot.id.clone(), at);
}
/// Show a particular child of a region.
pub fn show(&mut self, region: &str, at: usize) {
self.shown.insert(region.to_string(), at);
}
/// Whether a branch is open, given what its description says.
///
/// [`shown`](Self::shown)'s shape, one node kind along, and for the same
/// reason: the description says where the outline starts and the reader
/// says where it is now. See
/// [`Row::open`](quasi_router::Row::open) for why folding a branch asks the
/// app nothing.
#[must_use]
pub fn open(&self, key: &str, described: bool) -> bool {
self.opened.get(key).copied().unwrap_or(described)
}
/// Fold an open branch, or open a shut one.
pub fn fold(&mut self, key: &str, described: bool) {
let open = self.open(key, described);
self.opened.insert(key.to_string(), !open);
}
/// Whether this value is ticked.
#[must_use]
pub fn is_ticked(&self, value: &str) -> bool {
self.ticked.contains(value)
}
/// Tick it if it is not, untick it if it is.
///
/// Staging, never a write. Wiki `explicit-commit-affordance`: the commit
/// control is what locks a change in, and a tick that wrote on its own
/// would be the change happening with nothing to mark it.
pub fn tick(&mut self, value: &str) {
if !self.ticked.remove(value) {
self.ticked.insert(value.to_owned());
}
}
/// Everything ticked, in order.
///
/// Ordered because it is a `BTreeSet`, and that is worth relying on: a
/// handler reading [`Params::get_all`] gets the same sequence every run, so
/// a test over a bulk action is not sorting the answer first.
pub fn ticks(&self) -> impl Iterator {
self.ticked.iter().map(String::as_str)
}
/// Start the described ticks off, for the rows that arrive already ticked.
///
/// A description can say a row is ticked, and on a screen that has just
/// arrived that claim is the only thing there is. Applied on arrival rather
/// than read on every draw, because after that the user's ticks are the
/// truth and a redraw that went back to the description would undo them.
pub fn seed(&mut self, screen: &Screen) {
self.ticked = crate::focus::spots(screen, &Local::none())
.iter()
.filter_map(|spot| match spot {
Spot::Row {
ticked: Some(true),
value: Some(value),
..
} => Some(value.clone()),
_ => None,
})
.collect();
}
/// Forget everything typed and scrolled, and go back to the top.
///
/// What a whole new screen means. The boxes on it are different boxes, and
/// carrying a buffer across would put what was typed into a password field
/// into whatever field happens to share its name on the next screen. A
/// selection goes the same way and for the same reason: the rows are
/// different rows.
pub fn reset(&mut self) {
self.edits.clear();
self.scroll.clear();
self.ticked.clear();
self.shown.clear();
self.focus = 0;
self.inside = None;
// A screen that has arrived is the answer to whatever was outstanding,
// or is a different place entirely. Either way nothing on it has been
// pressed yet.
self.outstanding = None;
self.progress = Awaited::default();
}
/// The values a form submits, gathered for `names` in the order given.
///
/// A checkbox is here by presence, the way HTML submits one, so a box that
/// is not ticked sends nothing rather than sending an empty string. That is
/// [`Field::value`](quasi_router::Field::value)'s own convention read back
/// out.
#[must_use]
pub fn submission(&self, names: &[String], spots: &[Spot]) -> Params {
let mut params = Params::new();
for name in names {
let Some(field) = spots
.iter()
.filter_map(Spot::field)
.find(|field| &field.name == name)
else {
continue;
};
let value = self.typed(field);
if matches!(field.kind, layout::FieldKind::Checkbox)
&& value != quasi_router::Node::SELECTED
{
continue;
}
params.insert(name.clone(), value.to_string());
}
params
}
/// Drop anything held for a field the screen no longer has.
///
/// A fragment can replace a region holding half a form, and the buffers for
/// the fields that went away would otherwise ride along and be submitted by
/// the next form that happens to name one of them.
/// The frame is passed because the caret may legitimately be standing on
/// one of its verbs, which is a place past the end of the screen's own
/// spots. Clamping to the screen alone would take the caret off Send every
/// time a fragment landed anywhere on the page. The chrome is passed for
/// the same reason one lifetime along: a panel's controls sit past the
/// frame's verbs, and a field inside one is not the screen's to drop.
pub fn prune(&mut self, screen: &Screen, frame: &Frame, chrome: &Chrome) {
// Every described spot, including the ones inside a region that does
// not apply right now: a section the reader has toggled shut still
// holds what they typed into it, and dropping those buffers would lose
// the draft on the way back. `079a011e`.
let spots: Vec =
crate::focus::reaches_chromed(screen, frame, chrome, &Local::of(&Hidden::none(), self))
.into_iter()
.map(|reach| reach.spot)
.collect();
// The caret, though, is an index into what is on the screen.
let hidden = crate::reveal::hidden(screen, chrome, self);
let stops =
crate::focus::reaches_chromed(screen, frame, chrome, &Local::of(&hidden, self)).len();
let live: Vec<&str> = spots
.iter()
.filter_map(Spot::field)
.map(|field| field.name.as_str())
.collect();
self.edits.retain(|name, _| live.contains(&name.as_str()));
// The questions still on the screen that repeat, read back off the
// names their slots submit under. `60d1753c`: a count held for a
// question a fragment took away would decide how many boxes a later
// screen's question of the same name stands in.
//
// After the buffers, and reading the same walk: the walk was made with
// the counts still in place, so a slot the reader added is a live name
// and its buffer survives.
let repeating: Vec<&str> = spots
.iter()
.filter_map(|spot| match spot {
// The question's own controls name it whatever the reader has
// done, including having removed every slot: a count of zero is
// the state a walk over the boxes alone cannot see, and
// dropping it here would bring the slots back on the next
// fragment.
Spot::Repeat { field, .. } => Some(field.name.as_str()),
_ => None,
})
.collect();
self.slots
.retain(|name, _| repeating.contains(&name.as_str()));
let was = self.focus;
self.focus = self.focus.min(stops.saturating_sub(1));
// A fragment that moved the caret moved it to another stop, and the
// control it had stepped into belonged to the row it left. Clamping the
// index against the new row instead would keep the ring inside a row the
// user is no longer on.
if self.focus != was {
self.inside = None;
}
}
}